@vellumai/assistant 0.11.1 → 0.11.2-staging.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (318) hide show
  1. package/Dockerfile +1 -3
  2. package/README.md +1 -1
  3. package/eslint.config.mjs +28 -8
  4. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +1 -0
  5. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/__tests__/stripe-currency.test.ts +37 -0
  6. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/stripe-currency.ts +55 -0
  7. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +1 -0
  8. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/__tests__/stripe-currency.test.ts +37 -0
  9. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/stripe-currency.ts +55 -0
  10. package/node_modules/@vellumai/service-contracts/package.json +1 -0
  11. package/node_modules/@vellumai/service-contracts/src/__tests__/stripe-currency.test.ts +37 -0
  12. package/node_modules/@vellumai/service-contracts/src/stripe-currency.ts +55 -0
  13. package/node_modules/@vellumai/slack-text/src/index.test.ts +48 -0
  14. package/node_modules/@vellumai/slack-text/src/index.ts +25 -6
  15. package/openapi.yaml +292 -5
  16. package/package.json +1 -1
  17. package/src/__tests__/agent-loop-resume-interrupted.test.ts +223 -0
  18. package/src/__tests__/byok-default-profile-ensure.test.ts +43 -0
  19. package/src/__tests__/cli-logger-boundary-guard.test.ts +87 -0
  20. package/src/__tests__/compaction-events.test.ts +139 -2
  21. package/src/__tests__/config-loader-backfill.test.ts +35 -3
  22. package/src/__tests__/config-schema.test.ts +1 -0
  23. package/src/__tests__/conversation-agent-loop-fatal-cleanup.test.ts +30 -0
  24. package/src/__tests__/conversation-agent-loop.test.ts +180 -7
  25. package/src/__tests__/conversation-error.test.ts +211 -4
  26. package/src/__tests__/conversation-options-turn-scoped-transport.test.ts +64 -0
  27. package/src/__tests__/conversation-queue.test.ts +126 -0
  28. package/src/__tests__/conversation-retry-route.test.ts +46 -7
  29. package/src/__tests__/conversation-slash.test.ts +4 -2
  30. package/src/__tests__/conversation-summarize-route.test.ts +7 -1
  31. package/src/__tests__/conversation-summarize-up-to.test.ts +71 -5
  32. package/src/__tests__/document-create-dedupe.test.ts +2 -1
  33. package/src/__tests__/document-find-replace.test.ts +2 -1
  34. package/src/__tests__/document-tool-security.test.ts +2 -1
  35. package/src/__tests__/document-update-default-surface.test.ts +2 -1
  36. package/src/__tests__/document-workspace-file.test.ts +467 -0
  37. package/src/__tests__/emit-signal-routing-intent.test.ts +290 -49
  38. package/src/__tests__/guardian-card-withdrawal.test.ts +91 -3
  39. package/src/__tests__/http-user-message-parity.test.ts +66 -0
  40. package/src/__tests__/list-messages-attachments.test.ts +87 -0
  41. package/src/__tests__/list-messages-provider-error.test.ts +143 -0
  42. package/src/__tests__/list-messages-system-card.test.ts +100 -0
  43. package/src/__tests__/llm-resolver.test.ts +19 -9
  44. package/src/__tests__/managed-profile-guard.test.ts +23 -0
  45. package/src/__tests__/notification-platform-adapter.test.ts +130 -2
  46. package/src/__tests__/notification-telegram-adapter.test.ts +6 -0
  47. package/src/__tests__/notification-vellum-adapter.test.ts +45 -0
  48. package/src/__tests__/plugin-api-model-profiles.test.ts +10 -1
  49. package/src/__tests__/plugin-import-boundary-guard.test.ts +3 -0
  50. package/src/__tests__/provider-error-scenarios.test.ts +140 -0
  51. package/src/__tests__/provider-send-message-override-profile.test.ts +95 -0
  52. package/src/__tests__/run-conversation-turn-persistence.test.ts +66 -3
  53. package/src/__tests__/scripted-turn-metadata-persistence.test.ts +209 -0
  54. package/src/__tests__/skills.test.ts +27 -0
  55. package/src/__tests__/slack-channels-routes.test.ts +0 -2
  56. package/src/__tests__/slack-share-routes.test.ts +0 -3
  57. package/src/__tests__/slack-users-routes.test.ts +0 -2
  58. package/src/__tests__/subagent-tools.test.ts +11 -0
  59. package/src/__tests__/tool-preview-lifecycle.test.ts +58 -0
  60. package/src/__tests__/tool-result-spool.test.ts +5 -4
  61. package/src/__tests__/turn-boundary-resolution.test.ts +53 -0
  62. package/src/__tests__/turn-events-store.test.ts +26 -0
  63. package/src/__tests__/ui-visual-surface.test.ts +695 -0
  64. package/src/__tests__/unified-turn-context-visible-app.test.ts +99 -0
  65. package/src/__tests__/visible-app-context.test.ts +189 -0
  66. package/src/__tests__/workspace-git-service.test.ts +173 -33
  67. package/src/__tests__/workspace-migration-137-repair-retired-fireworks-minimax-model-id.test.ts +157 -0
  68. package/src/__tests__/workspace-migration-138-backfill-home-feed-titles.test.ts +373 -0
  69. package/src/__tests__/workspace-migration-139-clear-renamed-cost-profile-label.test.ts +137 -0
  70. package/src/agent/loop.ts +36 -7
  71. package/src/api/events/context-window-usage.ts +31 -0
  72. package/src/api/events/notification-intent.ts +8 -0
  73. package/src/api/events/ui-surface-pending.ts +35 -0
  74. package/src/api/index.ts +14 -0
  75. package/src/api/responses/conversation-message.ts +25 -4
  76. package/src/api/surfaces.ts +90 -1
  77. package/src/approvals/guardian-card-withdrawal.ts +66 -31
  78. package/src/approvals/guardian-decision-primitive.ts +4 -0
  79. package/src/calls/__tests__/call-setup-router.test.ts +156 -26
  80. package/src/calls/__tests__/voice-session-bridge.test.ts +201 -12
  81. package/src/calls/call-setup-router.ts +80 -36
  82. package/src/calls/voice-session-bridge.ts +173 -26
  83. package/src/cli/commands/inference.help.ts +3 -3
  84. package/src/cli/commands/notifications.help.ts +3 -2
  85. package/src/cli/commands/platform/__tests__/callback-routes-list.test.ts +42 -128
  86. package/src/cli/commands/platform/__tests__/credits.test.ts +9 -78
  87. package/src/cli/commands/platform/__tests__/helpers.ts +90 -0
  88. package/src/cli/commands/platform/__tests__/invoices.test.ts +238 -0
  89. package/src/cli/commands/platform/__tests__/plans.test.ts +9 -88
  90. package/src/cli/commands/platform/__tests__/status.test.ts +12 -87
  91. package/src/cli/commands/platform/__tests__/subscription.test.ts +9 -86
  92. package/src/cli/commands/platform/index.help.ts +92 -0
  93. package/src/cli/commands/platform/index.ts +7 -0
  94. package/src/cli/commands/platform/invoices.ts +132 -0
  95. package/src/cli/commands/usage.help.ts +1 -1
  96. package/src/cli/lib/list-installed-plugins.ts +2 -1
  97. package/src/config/__tests__/default-profile-catalog.test.ts +7 -10
  98. package/src/config/__tests__/deployment-context-defaults.test.ts +27 -5
  99. package/src/config/assistant-feature-flags.ts +7 -2
  100. package/src/config/bundled-skills/app-builder/SKILL.md +3 -2
  101. package/src/config/bundled-skills/subagent/SKILL.md +3 -1
  102. package/src/config/bundled-skills/subagent/TOOLS.json +3 -3
  103. package/src/config/bundled-skills/visualize/SKILL.md +163 -0
  104. package/src/config/call-site-defaults.ts +3 -2
  105. package/src/config/default-profile-catalog.ts +75 -70
  106. package/src/config/default-profile-names.ts +13 -28
  107. package/src/config/env-registry.ts +1 -0
  108. package/src/config/feature-flag-registry.json +16 -0
  109. package/src/config/llm-resolver.ts +21 -5
  110. package/src/config/loader.ts +18 -11
  111. package/src/config/schemas/llm.ts +1 -9
  112. package/src/config/schemas/memory-retrospective.ts +9 -0
  113. package/src/config/schemas/monitoring.ts +28 -2
  114. package/src/config/schemas/workspace-git.ts +15 -0
  115. package/src/config/seed-inference-profiles.ts +19 -5
  116. package/src/context/post-turn-tool-result-truncation.ts +2 -2
  117. package/src/conversations/__tests__/message-consolidation.test.ts +54 -0
  118. package/src/conversations/message-consolidation.ts +17 -15
  119. package/src/daemon/__tests__/turn-tail-assistant-reply-notify.test.ts +182 -0
  120. package/src/daemon/__tests__/turn-tail-deleted-conversation.test.ts +181 -0
  121. package/src/daemon/conversation-agent-loop-handlers.ts +90 -7
  122. package/src/daemon/conversation-agent-loop.ts +107 -22
  123. package/src/daemon/conversation-error.ts +169 -48
  124. package/src/daemon/conversation-messaging.ts +87 -4
  125. package/src/daemon/conversation-process.ts +57 -45
  126. package/src/daemon/conversation-runtime-assembly.ts +57 -0
  127. package/src/daemon/conversation-store.ts +36 -1
  128. package/src/daemon/conversation-surfaces.ts +68 -6
  129. package/src/daemon/conversation-turn-finalize.ts +76 -18
  130. package/src/daemon/conversation.ts +122 -27
  131. package/src/daemon/lifecycle.ts +9 -0
  132. package/src/daemon/message-types/conversations.ts +8 -0
  133. package/src/daemon/message-types/surfaces.ts +3 -0
  134. package/src/documents/document-store.ts +247 -10
  135. package/src/home/__tests__/feed-types.test.ts +43 -8
  136. package/src/home/__tests__/feed-writer.test.ts +59 -0
  137. package/src/home/feed-types.ts +34 -7
  138. package/src/home/feed-writer.ts +15 -6
  139. package/src/live-voice/__tests__/activity-label.test.ts +95 -0
  140. package/src/live-voice/__tests__/live-activity-reporter.test.ts +86 -5
  141. package/src/live-voice/__tests__/live-voice-agent-turn.test.ts +310 -25
  142. package/src/live-voice/__tests__/live-voice-events.test.ts +4 -4
  143. package/src/live-voice/__tests__/live-voice-triage-escalate.test.ts +8 -4
  144. package/src/live-voice/__tests__/live-voice-vad.test.ts +9 -1
  145. package/src/live-voice/activity-label.ts +169 -0
  146. package/src/live-voice/live-activity-reporter.ts +35 -6
  147. package/src/live-voice/live-voice-session.ts +282 -41
  148. package/src/live-voice/protocol.ts +35 -0
  149. package/src/messaging/providers/slack/__tests__/adapter-mention-rendering.test.ts +84 -2
  150. package/src/messaging/providers/slack/adapter.ts +86 -19
  151. package/src/messaging/providers/slack/api.test.ts +85 -1
  152. package/src/messaging/providers/slack/api.ts +121 -288
  153. package/src/messaging/providers/slack/client.ts +20 -251
  154. package/src/messaging/providers/slack/send.test.ts +4 -9
  155. package/src/messaging/providers/slack/send.ts +1 -1
  156. package/src/messaging/providers/slack/types.ts +5 -0
  157. package/src/messaging/providers/slack/web-api-transport.test.ts +181 -0
  158. package/src/messaging/providers/slack/web-api-transport.ts +367 -0
  159. package/src/messaging/providers/slack/withdraw.ts +6 -14
  160. package/src/messaging/providers/telegram-bot/send.test.ts +31 -1
  161. package/src/messaging/providers/telegram-bot/send.ts +23 -2
  162. package/src/messaging/providers/telegram-bot/withdraw.test.ts +152 -0
  163. package/src/messaging/providers/telegram-bot/withdraw.ts +166 -0
  164. package/src/monitoring/__tests__/db-integrity-sample.test.ts +18 -4
  165. package/src/monitoring/__tests__/file-descriptors.test.ts +144 -0
  166. package/src/monitoring/file-descriptors.ts +262 -0
  167. package/src/monitoring/process-memory.ts +4 -11
  168. package/src/monitoring/resource-sampler.ts +5 -0
  169. package/src/monitoring/worker.ts +12 -0
  170. package/src/notifications/__tests__/assistant-reply-producer.test.ts +740 -0
  171. package/src/notifications/__tests__/broadcaster.test.ts +347 -7
  172. package/src/notifications/__tests__/copy-composer.test.ts +53 -3
  173. package/src/notifications/__tests__/decision-engine.test.ts +349 -29
  174. package/src/notifications/__tests__/deterministic-checks.test.ts +21 -0
  175. package/src/notifications/__tests__/edit-notification.test.ts +330 -0
  176. package/src/notifications/__tests__/guardian-delivery-recorder.test.ts +71 -0
  177. package/src/notifications/__tests__/home-feed-side-effect.test.ts +170 -12
  178. package/src/notifications/adapters/macos.ts +3 -0
  179. package/src/notifications/adapters/platform.ts +90 -14
  180. package/src/notifications/adapters/telegram.ts +6 -4
  181. package/src/notifications/assistant-reply-producer.ts +219 -0
  182. package/src/notifications/broadcaster.ts +523 -317
  183. package/src/notifications/copy-composer.ts +27 -6
  184. package/src/notifications/decision-engine.ts +154 -77
  185. package/src/notifications/deterministic-checks.ts +6 -7
  186. package/src/notifications/edit-notification.ts +8 -4
  187. package/src/notifications/emit-signal.ts +49 -16
  188. package/src/notifications/guardian-delivery-recorder.ts +8 -2
  189. package/src/notifications/home-feed-side-effect.ts +43 -6
  190. package/src/notifications/notification-utils.ts +39 -4
  191. package/src/notifications/signal.ts +10 -0
  192. package/src/notifications/types.ts +17 -0
  193. package/src/persistence/bookmark-crud.ts +18 -9
  194. package/src/persistence/conversation-attention-store.ts +26 -0
  195. package/src/persistence/conversation-crud.ts +79 -32
  196. package/src/persistence/conversation-queries.ts +30 -15
  197. package/src/persistence/conversation-title-service.ts +5 -170
  198. package/src/persistence/conversation-types.ts +192 -1
  199. package/src/persistence/db-init.ts +14 -1
  200. package/src/persistence/db-maintenance.ts +5 -4
  201. package/src/persistence/embeddings/__tests__/plugin-index-qdrant-init.test.ts +219 -0
  202. package/src/persistence/embeddings/__tests__/plugin-index.test.ts +22 -0
  203. package/src/persistence/embeddings/__tests__/worker-script-version.test.ts +76 -0
  204. package/src/persistence/embeddings/embedding-local.ts +2 -1
  205. package/src/persistence/embeddings/embedding-runtime-manager.ts +29 -5
  206. package/src/persistence/embeddings/plugin-index.ts +84 -12
  207. package/src/persistence/migrations/360-add-document-workspace-path.test.ts +110 -0
  208. package/src/persistence/migrations/360-add-document-workspace-path.ts +39 -0
  209. package/src/persistence/planner-statistics.ts +14 -0
  210. package/src/persistence/schema/documents.ts +26 -11
  211. package/src/persistence/steps.ts +2 -0
  212. package/src/platform/client.test.ts +172 -2
  213. package/src/platform/client.ts +178 -55
  214. package/src/plugin-api/conversation-turn.ts +13 -0
  215. package/src/plugin-api/model-profiles.test.ts +6 -2
  216. package/src/plugins/defaults/memory/__tests__/bookmark-crud.test.ts +34 -0
  217. package/src/plugins/defaults/memory/__tests__/conversation-queries.test.ts +22 -0
  218. package/src/plugins/defaults/memory/__tests__/jobs-store-enqueue-gate.test.ts +11 -0
  219. package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +199 -0
  220. package/src/plugins/defaults/memory/__tests__/memory-retrospective-enqueue.test.ts +98 -1
  221. package/src/plugins/defaults/memory/__tests__/memory-retrospective-job.test.ts +105 -1
  222. package/src/plugins/defaults/memory/__tests__/memory-retrospective-sweep.test.ts +24 -2
  223. package/src/plugins/defaults/memory/__tests__/memory-retrospective-wake-chain.test.ts +524 -0
  224. package/src/plugins/defaults/memory/__tests__/memory-tier-boundary-guard.test.ts +1 -1
  225. package/src/plugins/defaults/memory/graph/__tests__/conversation-graph-memory-v2-routing.test.ts +9 -4
  226. package/src/plugins/defaults/memory/host-utils.ts +5 -0
  227. package/src/plugins/defaults/memory/memory-retrospective-accounting.ts +105 -1
  228. package/src/plugins/defaults/memory/memory-retrospective-enqueue.ts +64 -5
  229. package/src/plugins/defaults/memory/memory-retrospective-job.ts +36 -1
  230. package/src/plugins/defaults/memory/memory-retrospective-sweep.ts +12 -3
  231. package/src/plugins/defaults/memory/src/__tests__/memory-v2-simulate-route.test.ts +1 -0
  232. package/src/plugins/defaults/memory/substrate/__tests__/skill-store.test.ts +195 -1
  233. package/src/plugins/defaults/memory/substrate/consolidation-job.ts +1 -1
  234. package/src/plugins/defaults/memory/substrate/skill-store.ts +167 -29
  235. package/src/plugins/defaults/memory/v2/__tests__/injection.test.ts +131 -0
  236. package/src/plugins/defaults/memory/v2/__tests__/router.test.ts +1 -0
  237. package/src/plugins/defaults/memory/v2/activation-log-store.ts +4 -0
  238. package/src/plugins/defaults/memory/v2/injection.ts +48 -3
  239. package/src/plugins/defaults/memory/v2/rerank-local.ts +6 -2
  240. package/src/plugins/defaults/memory/v3/__tests__/pool-select.test.ts +1 -1
  241. package/src/plugins/defaults/platform-hosted/routes/reengage.ts +1 -1
  242. package/src/plugins/defaults/turn-context/injectors.ts +1 -0
  243. package/src/plugins/defaults/turn-context/unified-turn-context.ts +26 -0
  244. package/src/plugins/types.ts +17 -0
  245. package/src/prompts/templates/system-sections.ts +2 -2
  246. package/src/providers/__tests__/dispatch-connection-routing.test.ts +43 -2
  247. package/src/providers/__tests__/vellum-mismatch-routing.test.ts +8 -0
  248. package/src/providers/call-site-routing.ts +77 -14
  249. package/src/providers/connection-resolution.ts +42 -2
  250. package/src/providers/inference/__tests__/adapter-factory-openai-compatible.test.ts +127 -1
  251. package/src/providers/inference/adapter-factory.ts +70 -12
  252. package/src/providers/model-catalog.ts +0 -11
  253. package/src/providers/model-intents.ts +11 -0
  254. package/src/providers/registry.ts +8 -0
  255. package/src/providers/retry.ts +85 -17
  256. package/src/providers/types.ts +8 -1
  257. package/src/runtime/__tests__/agent-wake.test.ts +181 -0
  258. package/src/runtime/agent-wake.ts +132 -9
  259. package/src/runtime/channel-approval-types.ts +25 -0
  260. package/src/runtime/routes/__tests__/connection-routes-vs-cli-parity.test.ts +2 -2
  261. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +11 -0
  262. package/src/runtime/routes/__tests__/inference-provider-connection-routes.test.ts +121 -9
  263. package/src/runtime/routes/__tests__/platform-invoice-routes.test.ts +392 -0
  264. package/src/runtime/routes/__tests__/slack-channel-routes.test.ts +7 -0
  265. package/src/runtime/routes/__tests__/workspace-commit-routes.test.ts +63 -0
  266. package/src/runtime/routes/consolidation-routes.ts +1 -1
  267. package/src/runtime/routes/conversation-management-routes.ts +18 -6
  268. package/src/runtime/routes/conversation-query-routes.ts +14 -23
  269. package/src/runtime/routes/conversation-routes.ts +113 -8
  270. package/src/runtime/routes/credential-routes.ts +1 -5
  271. package/src/runtime/routes/documents-routes.ts +207 -12
  272. package/src/runtime/routes/identity-routes.ts +3 -93
  273. package/src/runtime/routes/inbound-stages/admission-policy.test.ts +31 -1
  274. package/src/runtime/routes/inbound-stages/admission-policy.ts +18 -0
  275. package/src/runtime/routes/inference-provider-connection-routes.ts +52 -3
  276. package/src/runtime/routes/notification-routes.ts +4 -1
  277. package/src/runtime/routes/platform-routes.ts +238 -3
  278. package/src/runtime/routes/playground/guard.ts +1 -2
  279. package/src/runtime/routes/slack-channel-routes.ts +2 -4
  280. package/src/runtime/routes/workspace-commit-routes.ts +49 -2
  281. package/src/runtime/routes/workspace-routes.ts +11 -12
  282. package/src/runtime/routes/workspace-utils.ts +49 -2
  283. package/src/runtime/services/conversation-serializer.ts +2 -4
  284. package/src/subagent/__tests__/consult-context-gating.test.ts +98 -0
  285. package/src/subagent/__tests__/consult-context-skills.test.ts +82 -0
  286. package/src/subagent/__tests__/consult-context.test.ts +84 -0
  287. package/src/subagent/__tests__/consult-prompt.test.ts +36 -0
  288. package/src/subagent/consult-context.ts +410 -0
  289. package/src/subagent/consult-prompt.ts +38 -5
  290. package/src/subagent/manager.ts +9 -4
  291. package/src/subagent/types.ts +8 -0
  292. package/src/telemetry/telemetry-event-sources.ts +7 -0
  293. package/src/telemetry/telemetry-wire-source.json +1 -1
  294. package/src/telemetry/telemetry-wire.generated.ts +1 -0
  295. package/src/telemetry/turn-events-store.ts +30 -1
  296. package/src/telemetry/types.ts +24 -0
  297. package/src/telemetry/usage-telemetry-reporter.test.ts +48 -0
  298. package/src/tools/browser/pinned-tabs.ts +3 -1
  299. package/src/tools/subagent/spawn.ts +23 -1
  300. package/src/tools/terminal/safe-env.ts +1 -0
  301. package/src/tools/ui-surface/definitions.ts +39 -1
  302. package/src/tools/ui-surface/surface-shape-docs.ts +38 -4
  303. package/src/tools/ui-surface/visual-validation.ts +787 -0
  304. package/src/tools/workflows/run-workflow.test.ts +1 -0
  305. package/src/util/__tests__/short-title.test.ts +229 -0
  306. package/src/util/__tests__/worker-compute.test.ts +65 -0
  307. package/src/util/cgroup-cpu.ts +93 -0
  308. package/src/util/errors.ts +27 -0
  309. package/src/util/process-tree.ts +19 -0
  310. package/src/util/short-title.ts +189 -0
  311. package/src/util/worker-compute.ts +83 -0
  312. package/src/workspace/byok-default-profile-ensure.ts +25 -15
  313. package/src/workspace/git-service.ts +79 -30
  314. package/src/workspace/migrations/137-repair-retired-fireworks-minimax-model-id.ts +131 -0
  315. package/src/workspace/migrations/138-backfill-home-feed-titles.ts +179 -0
  316. package/src/workspace/migrations/139-clear-renamed-cost-profile-label.ts +92 -0
  317. package/src/workspace/migrations/registry.ts +6 -0
  318. package/src/plugins/defaults/memory/substrate/constants.ts +0 -8
@@ -12,9 +12,11 @@ import {
12
12
  isImageUnprocessableError,
13
13
  } from "../plugins/defaults/image-recovery/detect.js";
14
14
  import { ConnectionResolutionError } from "../providers/connection-resolution.js";
15
+ import { PROVIDER_CATALOG } from "../providers/model-catalog.js";
15
16
  import { getProviderRoutingSource } from "../providers/registry.js";
16
17
  import { isAbortReason } from "../util/abort-reasons.js";
17
18
  import {
19
+ type ProviderCredentialSource,
18
20
  ProviderError,
19
21
  type ProviderErrorReason,
20
22
  ProviderNotConfiguredError,
@@ -63,6 +65,14 @@ export interface ClassifiedConversationError {
63
65
  export interface ConversationErrorAttribution {
64
66
  connectionName?: string;
65
67
  profileName?: string;
68
+ /** Whether the resolved turn route uses Vellum-managed inference. */
69
+ isManagedRoute?: boolean;
70
+ /**
71
+ * Which credential the failed request presented. Splits the non-managed side
72
+ * of `isManagedRoute` into personal keys, subscription logins, and keyless
73
+ * endpoints so rejection copy names the right thing to fix.
74
+ */
75
+ credentialSource?: ProviderCredentialSource;
66
76
  }
67
77
 
68
78
  // Network-level error patterns (connection refused, timeout, DNS, reset)
@@ -181,6 +191,11 @@ export interface ErrorContext {
181
191
  * underlying connection name is generic.
182
192
  */
183
193
  profileName?: string;
194
+ /**
195
+ * Whether the resolved turn route uses Vellum-managed inference. Turn routing
196
+ * takes precedence over the provider registry's boot-time default.
197
+ */
198
+ isManagedRoute?: boolean;
184
199
  }
185
200
 
186
201
  /**
@@ -253,9 +268,22 @@ export function classifyConversationError(
253
268
  // credential-related classifications can name the exact slot to fix.
254
269
  // A `ProviderNotConfiguredError` instance carries its own attribution
255
270
  // (from the throw site) which takes priority over context when present.
271
+ // The failed call's own route wins field by field — it is resolved at
272
+ // dispatch, while context describes the turn and can be stale for this
273
+ // call. Per-field, so a route carrying only some fields doesn't blank the
274
+ // rest.
275
+ const providerRoute =
276
+ error instanceof ProviderError ? error.routeAttribution : undefined;
277
+ const connectionName = providerRoute?.connectionName ?? ctx.connectionName;
278
+ const profileName = providerRoute?.profileName ?? ctx.profileName;
279
+ const isManagedRoute = providerRoute?.isManagedRoute ?? ctx.isManagedRoute;
256
280
  const attribution: ConversationErrorAttribution = {
257
- ...(ctx.connectionName ? { connectionName: ctx.connectionName } : {}),
258
- ...(ctx.profileName ? { profileName: ctx.profileName } : {}),
281
+ ...(connectionName ? { connectionName } : {}),
282
+ ...(profileName ? { profileName } : {}),
283
+ ...(isManagedRoute !== undefined ? { isManagedRoute } : {}),
284
+ ...(providerRoute?.credentialSource
285
+ ? { credentialSource: providerRoute.credentialSource }
286
+ : {}),
259
287
  };
260
288
 
261
289
  // Dedicated classification for missing provider API key
@@ -354,15 +382,28 @@ function classifyCore(
354
382
  message: string,
355
383
  attribution: ConversationErrorAttribution = {},
356
384
  ): Omit<ClassifiedConversationError, "debugDetails"> {
385
+ const isManagedRoute =
386
+ error instanceof ProviderError &&
387
+ (attribution.isManagedRoute ??
388
+ getProviderRoutingSource(error.provider) === "managed-proxy");
389
+ // Which credential the request actually presented. Only rejection copy needs
390
+ // this finer axis; every other branch keys off `isManagedRoute`. Routes that
391
+ // predate per-request stamping collapse to the managed/personal split.
392
+ const credentialSource: ProviderCredentialSource =
393
+ attribution.credentialSource ??
394
+ (isManagedRoute ? "vellum-managed" : "byok");
395
+
357
396
  // Prefer the semantic reason stamped by the provider layer, regardless of
358
397
  // HTTP status — statusless errors (e.g. SDK streaming failures) still carry a
359
398
  // reason. Reasons that map cleanly win here; `bad_request`/`unknown` (and a
360
399
  // reason-less error) fall through to the status switch + regex battery below.
361
400
  if (error instanceof ProviderError && error.reason) {
362
401
  const c = reasonToClassification(error.reason, {
363
- routingSource: getProviderRoutingSource(error.provider),
402
+ isManagedRoute,
403
+ credentialSource,
364
404
  attribution,
365
405
  message,
406
+ providerName: error.provider,
366
407
  });
367
408
  if (c) {
368
409
  return c;
@@ -376,26 +417,24 @@ function classifyCore(
376
417
  return contextTooLargeClassification();
377
418
  }
378
419
  if (error.statusCode === 401 || error.statusCode === 403) {
379
- // Both managed-proxy and user-key 401/403s reach this branch.
380
- // Managed-proxy routes through the assistant API key; if that
381
- // credential is stale, the user cannot fix it from model settings.
382
- // Everything else is a user-set credential that the upstream provider
383
- // rejected, so emit `PROVIDER_INVALID_KEY` and let the chat banner point
384
- // at Settings.
385
- const providerName = error.provider;
386
- if (getProviderRoutingSource(providerName) === "managed-proxy") {
387
- return managedKeyInvalidClassification();
388
- }
389
- return invalidApiKeyClassification(attribution);
420
+ // Managed routes through the assistant API key; if that credential is
421
+ // stale, the user cannot fix it from model settings. Everything else is
422
+ // a credential the user owns, so the copy names which one to update and
423
+ // the chat banner points at Settings.
424
+ return rejectedCredentialClassification(
425
+ error.provider,
426
+ credentialSource,
427
+ attribution,
428
+ );
390
429
  }
391
430
  if (error.statusCode === 402) {
392
- if (isManagedBalanceError(error)) {
431
+ if (isManagedRoute) {
393
432
  return managedBalanceClassification();
394
433
  }
395
434
  return providerBillingClassification();
396
435
  }
397
436
  if (error.statusCode === 429) {
398
- if (isManagedUsageLimitError(error, message)) {
437
+ if (isManagedUsageLimitError(message, isManagedRoute)) {
399
438
  return managedUsageLimitClassification();
400
439
  }
401
440
  return rateLimitClassification();
@@ -452,7 +491,11 @@ function classifyCore(
452
491
  // Mirror the 401/403 branch: a credential-shaped 4xx is an
453
492
  // "invalid key" surface (banner: "Invalid API key"), distinct
454
493
  // from "no key configured" (banner: "API key required").
455
- return invalidApiKeyClassification(attribution);
494
+ return rejectedCredentialClassification(
495
+ error.provider,
496
+ credentialSource,
497
+ attribution,
498
+ );
456
499
  }
457
500
  if (isImageDimensionsTooLargeError(message)) {
458
501
  return {
@@ -508,7 +551,7 @@ function classifyCore(
508
551
  }
509
552
 
510
553
  // Regex fallback for non-ProviderError or ProviderError without statusCode
511
- return classifyByMessage(error, message);
554
+ return classifyByMessage(message, isManagedRoute);
512
555
  }
513
556
 
514
557
  /**
@@ -540,17 +583,21 @@ function extractProviderDetail(message: string): string | undefined {
540
583
  function reasonToClassification(
541
584
  reason: ProviderErrorReason,
542
585
  args: {
543
- routingSource?: string;
586
+ isManagedRoute: boolean;
587
+ credentialSource: ProviderCredentialSource;
544
588
  attribution?: ConversationErrorAttribution;
545
589
  message: string;
590
+ providerName: string;
546
591
  },
547
592
  ): Omit<ClassifiedConversationError, "debugDetails"> | null {
548
- const managed = args.routingSource === "managed-proxy";
593
+ const managed = args.isManagedRoute;
549
594
  switch (reason) {
550
595
  case "invalid_credentials":
551
- return managed
552
- ? managedKeyInvalidClassification()
553
- : invalidApiKeyClassification(args.attribution);
596
+ return rejectedCredentialClassification(
597
+ args.providerName,
598
+ args.credentialSource,
599
+ args.attribution,
600
+ );
554
601
  case "rate_limited":
555
602
  // Match managed usage-limit body patterns, as the legacy path does.
556
603
  if (
@@ -670,18 +717,13 @@ function isStreamingError(message: string): boolean {
670
717
  return STREAMING_ERROR_PATTERNS.some((p) => p.test(message));
671
718
  }
672
719
 
673
- function isManagedUsageLimitError(error: unknown, message: string): boolean {
674
- if (
675
- error instanceof ProviderError &&
676
- getProviderRoutingSource(error.provider) === "managed-proxy"
677
- ) {
678
- return true;
679
- }
680
- return MANAGED_USAGE_LIMIT_PATTERNS.some((p) => p.test(message));
681
- }
682
-
683
- function isManagedBalanceError(error: ProviderError): boolean {
684
- return getProviderRoutingSource(error.provider) === "managed-proxy";
720
+ function isManagedUsageLimitError(
721
+ message: string,
722
+ isManagedRoute: boolean,
723
+ ): boolean {
724
+ return (
725
+ isManagedRoute || MANAGED_USAGE_LIMIT_PATTERNS.some((p) => p.test(message))
726
+ );
685
727
  }
686
728
 
687
729
  function isProviderBillingError(message: string): boolean {
@@ -694,8 +736,14 @@ function managedBalanceClassification(): Omit<
694
736
  > {
695
737
  return {
696
738
  code: "PROVIDER_BILLING",
739
+ // This classification feeds two surfaces with different semantics: the
740
+ // terminal provider-error path (the turn ends with no reply) and the
741
+ // non-terminal memory-v3 degraded notice (a normal reply still follows).
742
+ // Keep the wording context-neutral so it is true in both places; the
743
+ // terminal persist site in conversation-agent-loop.ts swaps in
744
+ // assistant-voice copy for the synthetic assistant row.
697
745
  userMessage:
698
- "You've run out of credits. Add funds to continue using the assistant.",
746
+ "You're out of credits. Add credits in Settings Billing to continue.",
699
747
  retryable: false,
700
748
  errorCategory: "credits_exhausted",
701
749
  };
@@ -756,13 +804,21 @@ function managedUsageLimitClassification(): Omit<
756
804
  };
757
805
  }
758
806
 
807
+ /**
808
+ * Deliberately instruction-free. The assistant API key is provisioned and
809
+ * pushed by the platform — the assistant only ever reads it — so there is no
810
+ * Settings affordance to re-provision it and no user action that would help.
811
+ * The copy's job is to rule out the user's own provider key as the cause;
812
+ * clients that can offer a real next step attach one (web renders Doctor).
813
+ */
759
814
  function managedKeyInvalidClassification(): Omit<
760
815
  ClassifiedConversationError,
761
816
  "debugDetails"
762
817
  > {
763
818
  return {
764
819
  code: "MANAGED_KEY_INVALID",
765
- userMessage: "Couldn't refresh assistant credentials.",
820
+ userMessage:
821
+ "Vellum's managed inference credential was rejected. This isn't a personal provider API key — Vellum provisions this one, so there's nothing to update in Settings.",
766
822
  retryable: false,
767
823
  errorCategory: "managed_key_invalid",
768
824
  };
@@ -845,6 +901,76 @@ function describeAttribution(
845
901
  return "";
846
902
  }
847
903
 
904
+ function providerDisplayName(providerName: string): string {
905
+ return (
906
+ PROVIDER_CATALOG.find((provider) => provider.id === providerName)
907
+ ?.displayName ?? providerName
908
+ );
909
+ }
910
+
911
+ function classificationAttribution(
912
+ attribution: ConversationErrorAttribution | undefined,
913
+ ): Pick<ClassifiedConversationError, "connectionName" | "profileName"> {
914
+ return {
915
+ ...(attribution?.connectionName
916
+ ? { connectionName: attribution.connectionName }
917
+ : {}),
918
+ ...(attribution?.profileName
919
+ ? { profileName: attribution.profileName }
920
+ : {}),
921
+ };
922
+ }
923
+
924
+ function rejectedCredentialClassification(
925
+ providerName: string,
926
+ credentialSource: ProviderCredentialSource,
927
+ attribution?: ConversationErrorAttribution,
928
+ ): Omit<ClassifiedConversationError, "debugDetails"> {
929
+ switch (credentialSource) {
930
+ case "vellum-managed":
931
+ return managedKeyInvalidClassification();
932
+ case "oauth-subscription":
933
+ return subscriptionLoginRejectedClassification(providerName, attribution);
934
+ case "no-auth":
935
+ return endpointAuthenticationRequiredClassification(
936
+ providerName,
937
+ attribution,
938
+ );
939
+ case "byok":
940
+ return invalidApiKeyClassification(providerName, attribution);
941
+ }
942
+ }
943
+
944
+ function endpointAuthenticationRequiredClassification(
945
+ providerName: string,
946
+ attribution?: ConversationErrorAttribution,
947
+ ): Omit<ClassifiedConversationError, "debugDetails"> {
948
+ const provider = providerDisplayName(providerName);
949
+ const target = describeAttribution(attribution);
950
+ return {
951
+ code: "PROVIDER_API",
952
+ userMessage: `The ${provider} endpoint${target} requires authentication, but that connection is configured without credentials. Configure authentication for that endpoint in Settings → Models & Services.`,
953
+ retryable: false,
954
+ errorCategory: "provider_endpoint_auth_required",
955
+ ...classificationAttribution(attribution),
956
+ };
957
+ }
958
+
959
+ function subscriptionLoginRejectedClassification(
960
+ providerName: string,
961
+ attribution?: ConversationErrorAttribution,
962
+ ): Omit<ClassifiedConversationError, "debugDetails"> {
963
+ const provider = providerDisplayName(providerName);
964
+ const target = describeAttribution(attribution);
965
+ return {
966
+ code: "PROVIDER_API",
967
+ userMessage: `Your ${provider} subscription login${target} was rejected by ${provider}. Reconnect that account in Settings → Models & Services.`,
968
+ retryable: false,
969
+ errorCategory: "provider_subscription_auth",
970
+ ...classificationAttribution(attribution),
971
+ };
972
+ }
973
+
848
974
  /**
849
975
  * Classification for an invalid (rejected by the upstream provider, e.g.
850
976
  * Anthropic 401/403) API key. Distinct from `PROVIDER_NOT_CONFIGURED`
@@ -853,22 +979,17 @@ function describeAttribution(
853
979
  * different recovery actions (update vs. add).
854
980
  */
855
981
  function invalidApiKeyClassification(
982
+ providerName: string,
856
983
  attribution?: ConversationErrorAttribution,
857
984
  ): Omit<ClassifiedConversationError, "debugDetails"> {
985
+ const provider = providerDisplayName(providerName);
858
986
  const target = describeAttribution(attribution);
859
987
  return {
860
988
  code: "PROVIDER_INVALID_KEY",
861
- userMessage: target
862
- ? `The API key${target} was rejected by the provider. Update it in Settings → Models & Services.`
863
- : "Your API key was rejected by the provider. Update it in Settings → Models & Services.",
989
+ userMessage: `Your personal ${provider} API key${target} was rejected by ${provider}. Update that key in Settings → Models & Services.`,
864
990
  retryable: false,
865
991
  errorCategory: "provider_invalid_key",
866
- ...(attribution?.connectionName
867
- ? { connectionName: attribution.connectionName }
868
- : {}),
869
- ...(attribution?.profileName
870
- ? { profileName: attribution.profileName }
871
- : {}),
992
+ ...classificationAttribution(attribution),
872
993
  };
873
994
  }
874
995
 
@@ -906,8 +1027,8 @@ function providerNotConfiguredClassification(
906
1027
  * short-circuit and the status switch both decline to classify.
907
1028
  */
908
1029
  function classifyByMessage(
909
- error: unknown,
910
1030
  message: string,
1031
+ isManagedRoute: boolean,
911
1032
  ): Omit<ClassifiedConversationError, "debugDetails"> {
912
1033
  // Empty-request-messages is always an internal construction failure — check
913
1034
  // it before the provider/network patterns so a ProviderError that carries the
@@ -924,7 +1045,7 @@ function classifyByMessage(
924
1045
  // Check rate limit first (before network, since 429 could match both)
925
1046
  for (const pattern of RATE_LIMIT_PATTERNS) {
926
1047
  if (pattern.test(message)) {
927
- if (isManagedUsageLimitError(error, message)) {
1048
+ if (isManagedUsageLimitError(message, isManagedRoute)) {
928
1049
  return managedUsageLimitClassification();
929
1050
  }
930
1051
  return rateLimitClassification();
@@ -670,6 +670,35 @@ export interface PersistMessageOptions {
670
670
  * memory or search; see `ProcessMessageOptions.skipUserMessageIndexing`.
671
671
  */
672
672
  skipIndexing?: boolean;
673
+ /**
674
+ * True when this turn was auto-sent on the user's behalf rather than typed
675
+ * by them: onboarding research prompts, the personality `<system-message>`,
676
+ * research corrections, hidden kickoff greetings, the legacy pre-chat
677
+ * bootstrap, and `[User action on ...]` surface synthetics.
678
+ *
679
+ * Stamped onto `messages.metadata.scripted` and forwarded to
680
+ * `TurnTelemetryEvent.scripted`, where activation metrics exclude it. This
681
+ * is the consent-independent replacement for classifying turns by
682
+ * text-matching their content in diagnostics-gated traces. That classifier
683
+ * can only see owners who opted into diagnostics, so it silently counted
684
+ * scripted turns as real messages for everyone else (ANT-10).
685
+ *
686
+ * Defaults to `false`: a daemon that knows about the field asserts "the user
687
+ * typed this" for ordinary sends, which is what makes a user's activation
688
+ * measurable. This is only safe because every auto-send path is marked at
689
+ * its source. See the merged-metadata note below for the list.
690
+ *
691
+ * Callers persisting machine-authored content into a `standard` conversation
692
+ * MUST pass `true`. A wrong `false` is trusted downstream and re-inflates
693
+ * activation. (Machine-authored turns in `background` / `scheduled`
694
+ * conversations are already excluded from activation by conversation type,
695
+ * and the `assert_scripted_signals_agree` dbt test catches any straggler
696
+ * whose text matches a known template.)
697
+ *
698
+ * May also be carried in the `metadata` bag, which is how queued sends
699
+ * thread it: the queue round-trips `metadata`, not these options.
700
+ */
701
+ scripted?: boolean;
673
702
  }
674
703
 
675
704
  // ── persistUserMessage ───────────────────────────────────────────────
@@ -805,15 +834,46 @@ export async function persistQueuedMessageBody(
805
834
  // The caller-supplied metadata may include it (channel ingress threads it
806
835
  // through `Server.processMessage`); we materialize it into the typed
807
836
  // `slackMeta` sub-key below when the turn channel is Slack.
808
- const { slackInbound: rawSlackInbound, ...metadataWithoutSlackInbound } =
809
- (metadata ?? {}) as Record<string, unknown> & {
810
- slackInbound?: SlackInboundMessageMetadata;
811
- };
837
+ // `scripted` is pulled out of the raw bag alongside `slackInbound` so the
838
+ // spread below can never re-introduce an unvalidated value. Letting a
839
+ // non-boolean through would be worse than dropping it: sqlite stores it
840
+ // verbatim, and `turn-events-store` narrows anything that isn't 1 to
841
+ // `false`, turning a junk string into a confident "the user typed this".
842
+ const {
843
+ slackInbound: rawSlackInbound,
844
+ scripted: rawScriptedFromMetadata,
845
+ ...metadataWithoutSlackInbound
846
+ } = (metadata ?? {}) as Record<string, unknown> & {
847
+ slackInbound?: SlackInboundMessageMetadata;
848
+ scripted?: unknown;
849
+ };
812
850
  const slackMeta = buildSlackMetaForPersistence({
813
851
  slackInbound: rawSlackInbound,
814
852
  turnChannel: turnCtx?.userMessageChannel,
815
853
  });
816
854
 
855
+ // See the `scripted` note on the merged metadata below. Only a real
856
+ // boolean in the bag counts: a stray truthy string must not be read as a
857
+ // scripted assertion.
858
+ const scriptedFromMetadata =
859
+ typeof rawScriptedFromMetadata === "boolean"
860
+ ? rawScriptedFromMetadata
861
+ : undefined;
862
+ // `automated` (machine-authored, set by the messaging skill and the memory
863
+ // skill-card) implies scripted: it is by definition not a turn the user
864
+ // typed. Only a DEFAULT: an explicit `scripted` wins, so a caller can
865
+ // mark an automated message as a real turn if that is ever right. Note the
866
+ // two flags are not interchangeable in the other direction: `automated`
867
+ // also suppresses memory extraction, so scripted onboarding turns that
868
+ // should still be indexed must not be marked automated to get counted out.
869
+ const scriptedFromAutomated =
870
+ metadataWithoutSlackInbound.automated === true ? true : undefined;
871
+ const resolvedScripted =
872
+ options.scripted ??
873
+ scriptedFromMetadata ??
874
+ scriptedFromAutomated ??
875
+ false;
876
+
817
877
  // Client attribution for turn telemetry, stored under the `client`
818
878
  // metadata bag which `turn-events-store` forwards onto
819
879
  // `TurnTelemetryEvent.client`. The bag merges two sources per key:
@@ -853,6 +913,29 @@ export async function persistQueuedMessageBody(
853
913
  ...clientBag,
854
914
  ...(imageSourcePaths ? { imageSourcePaths } : {}),
855
915
  ...(slackMeta ? { slackMeta } : {}),
916
+ // Scripted-turn marker, forwarded by `turn-events-store` onto
917
+ // `TurnTelemetryEvent.scripted`. Written LAST so it cannot be
918
+ // half-overwritten by the raw metadata spread above.
919
+ //
920
+ // Resolved from the typed option first, then the metadata bag. The bag
921
+ // is how queued sends carry it, since the queue round-trips `metadata`
922
+ // but not `PersistMessageOptions` (same carrier as the `hidden` flag).
923
+ //
924
+ // Always stamped, including the `false` default: a daemon that knows
925
+ // about the field asserts "the user typed this" for ordinary sends, and
926
+ // that assertion is what makes a user's activation MEASURABLE
927
+ // downstream. Absent would mean "unknown", which is strictly worse
928
+ // information than a truthful false.
929
+ //
930
+ // Defaulting to false is only safe because every auto-send path is now
931
+ // marked at its source: the web onboarding flows (research prompt,
932
+ // kickoff, personality, corrections, legacy bootstrap), `[User action
933
+ // on ...]` surface synthetics, and anything flagged `automated`. A new
934
+ // auto-send path that forgets to mark itself lands here as a false and
935
+ // is believed. The `assert_scripted_signals_agree` dbt test is the
936
+ // backstop: it fires when a turn claiming `false` matches a known
937
+ // scripted template.
938
+ scripted: resolvedScripted,
856
939
  };
857
940
 
858
941
  // Materialize each attachment into an attachment-store row up front so the
@@ -24,12 +24,14 @@ import { extractPreferences } from "../notifications/preference-extractor.js";
24
24
  import { createPreference } from "../notifications/preferences-store.js";
25
25
  import {
26
26
  addMessage,
27
+ isEchoSuppressedUserMessage,
27
28
  isHiddenMessageMetadata,
28
29
  provenanceFromTrustContext,
29
30
  recordConversationPersistedSeq,
30
31
  setConversationOriginChannelIfUnset,
31
32
  setConversationOriginInterfaceIfUnset,
32
33
  } from "../persistence/conversation-crud.js";
34
+ import { isReplyPushIneligibleUserMessage } from "../persistence/conversation-types.js";
33
35
  import type { ContextWindowResult } from "../plugins/defaults/compaction/window-manager.js";
34
36
  import { getCurrentSeq } from "../runtime/assistant-stream-state.js";
35
37
  import {
@@ -63,48 +65,6 @@ import { resolveVerificationSessionIntent } from "./verification-session-intent.
63
65
 
64
66
  const log = getLogger("conversation-process");
65
67
 
66
- /**
67
- * Daemon-injected run lifecycle notifications — subagent (`subagentNotification`),
68
- * ACP run (`acpNotification`), and any wake trigger (the persisted
69
- * `<background_event source="...">` row every wake reads) — are persisted into
70
- * the parent conversation so the orchestrator wakes and reads the trigger, but
71
- * they are internal scaffolding: the user sees the wake through its inline card
72
- * ("Conversation Woke", or a terminal card for a backgrounded bash run), not a
73
- * chat turn. Skip the `user_message_echo` broadcast for these so they never
74
- * render as a live user bubble; the persisted row is filtered from the rendered
75
- * transcript on the client.
76
- *
77
- * Messages explicitly flagged `hidden` (a hidden `POST /messages` send that
78
- * queued behind an in-flight turn, e.g. the channel-setup wizard-close
79
- * marker) are suppressed the same way — the immediate route path already
80
- * skips their echo, and the persisted `hidden` metadata keeps them out of
81
- * the fetched transcript.
82
- */
83
- export function isEchoSuppressedUserMessage(
84
- metadata: Record<string, unknown> | undefined,
85
- ): boolean {
86
- return (
87
- isHiddenMessageMetadata(metadata) ||
88
- metadata?.subagentNotification != null ||
89
- metadata?.acpNotification != null ||
90
- isBackgroundEventMetadata(metadata)
91
- );
92
- }
93
-
94
- /**
95
- * True when the row is a persisted `<background_event source="...">` trigger —
96
- * every wake, scheduled run, and backgrounded-tool completion stamps one (see
97
- * {@link persistWakeTriggerMessage}). The permission mode such a turn ran under
98
- * varies (most run interactive; clientless/headless wakes do not) and is
99
- * recorded separately in `backgroundEventInteractive`; this predicate only
100
- * identifies the row as a background event.
101
- */
102
- export function isBackgroundEventMetadata(
103
- metadata: Record<string, unknown> | undefined,
104
- ): boolean {
105
- return typeof metadata?.backgroundEventSource === "string";
106
- }
107
-
108
68
  /** Locale-formatted count for the user-facing context stats cards. */
109
69
  const fmt = (n: number | undefined) => (n ?? 0).toLocaleString("en-US");
110
70
 
@@ -331,6 +291,12 @@ async function buildPassthroughBatch(
331
291
  if (candidate.transport?.clientOs !== head.transport?.clientOs) {
332
292
  break;
333
293
  }
294
+ // Same head-wins problem for the app on screen: batching a message sent
295
+ // while a different app was open would run it against the head's
296
+ // `visible_app`, pointing "the app" at the wrong one.
297
+ if (candidate.transport?.visibleAppId !== head.transport?.visibleAppId) {
298
+ break;
299
+ }
334
300
  if (candidate.sourceActorPrincipalId !== head.sourceActorPrincipalId) {
335
301
  break;
336
302
  }
@@ -769,6 +735,7 @@ async function drainSingleMessage(
769
735
  conversation.applyHostEnvFromTransport(next.transport);
770
736
  conversation.applyClientTimezoneFromTransport(next.transport);
771
737
  conversation.applyClientOsFromTransport(next.transport);
738
+ conversation.applyVisibleAppFromTransport(next.transport);
772
739
  }
773
740
 
774
741
  conversation.currentTurnAuthContext = next.authContext;
@@ -963,7 +930,11 @@ async function drainSingleMessage(
963
930
  conversation.emitActivityState("thinking", "context_compacting", {
964
931
  requestId: next.requestId,
965
932
  });
966
- const result = await conversation.forceCompact();
933
+ // Push the usage refresh to the queued item's own sink, the one the
934
+ // result card below streams on. `sendToClient` is reset to a no-op once
935
+ // an interactive turn finishes, so a `/compact` draining behind one
936
+ // would otherwise refresh nothing.
937
+ const result = await conversation.forceCompact(next.onEvent);
967
938
  const responseText = formatCompactResult(result);
968
939
 
969
940
  const assistantMsg = createAssistantMessage(responseText);
@@ -1345,6 +1316,7 @@ async function drainBatch(
1345
1316
  conversation.applyHostEnvFromTransport(head.transport);
1346
1317
  conversation.applyClientTimezoneFromTransport(head.transport);
1347
1318
  conversation.applyClientOsFromTransport(head.transport);
1319
+ conversation.applyVisibleAppFromTransport(head.transport);
1348
1320
  }
1349
1321
 
1350
1322
  conversation.currentTurnAuthContext = head.authContext;
@@ -1392,6 +1364,11 @@ async function drainBatch(
1392
1364
  let lastSuccessfulCurrentPage: string | undefined;
1393
1365
  let lastSuccessfulContent: string | undefined;
1394
1366
  let lastUserMessageId: string | undefined;
1367
+ // `messages.id` of the last member the reply-push producer would actually
1368
+ // notify about. Selected with the producer's own eligibility predicate so a
1369
+ // trailing row it suppresses (a hidden marker, a channel send) cannot stand
1370
+ // in for the prompt ahead of it and swallow that prompt's push.
1371
+ let lastPushEligibleUserMessageId: string | undefined;
1395
1372
  // Members whose persist succeeded. `fanOutOnEvent` below must only
1396
1373
  // broadcast agent-loop events to these — clients whose persist failed
1397
1374
  // already received an error event and must not also receive the
@@ -1557,6 +1534,10 @@ async function drainBatch(
1557
1534
  continue;
1558
1535
  }
1559
1536
 
1537
+ if (!isReplyPushIneligibleUserMessage(qm.metadata)) {
1538
+ lastPushEligibleUserMessageId = lastUserMessageId;
1539
+ }
1540
+
1560
1541
  // Broadcast the user message to all hub subscribers so passive devices
1561
1542
  // see each batched user turn before the assistant reply starts streaming.
1562
1543
  if (!isEchoSuppressedUserMessage(qm.metadata)) {
@@ -1693,7 +1674,13 @@ async function drainBatch(
1693
1674
  isUserMessage?: boolean;
1694
1675
  titleText?: string;
1695
1676
  isHiddenPrompt?: boolean;
1696
- } = { isUserMessage: true };
1677
+ notifyUserMessageId?: string;
1678
+ } = {
1679
+ isUserMessage: true,
1680
+ };
1681
+ if (lastPushEligibleUserMessageId !== undefined) {
1682
+ drainLoopOptions.notifyUserMessageId = lastPushEligibleUserMessageId;
1683
+ }
1697
1684
  // Source interactive flag from the last successfully-persisted sibling so
1698
1685
  // a trailing failed tail doesn't flip the agent loop's interactivity.
1699
1686
  const lastSuccessfulBatchEntry =
@@ -1761,6 +1748,26 @@ export interface ProcessMessageOptions {
1761
1748
  displayContent?: string;
1762
1749
  /** JWT-verified committer principal for turn-scoped host-proxy authorization. */
1763
1750
  sourceActorPrincipalId?: string;
1751
+ /**
1752
+ * True when this turn was auto-sent on the user's behalf rather than typed
1753
+ * (see `PersistMessageOptions.scripted`). Forwarded to persistence so the
1754
+ * turn is excluded from activation counts. Defaults to false. A caller
1755
+ * sending machine-authored content into a `standard` conversation must set
1756
+ * it explicitly.
1757
+ *
1758
+ * Related to `metadata.automated` below but not the same knob: `automated`
1759
+ * implies scripted (machine-authored is by definition not typed), while
1760
+ * `scripted` carries no memory-indexing side effect. A caller that wants a
1761
+ * turn excluded from activation but still indexed sets this, not that.
1762
+ */
1763
+ scripted?: boolean;
1764
+ /**
1765
+ * Extra metadata stamped onto the persisted user row alongside the channel
1766
+ * and provenance fields the turn derives. Callers that drive a turn on
1767
+ * someone's behalf use it to mark the row's provenance (e.g. the plugin-api
1768
+ * facade stamps `automated`).
1769
+ */
1770
+ metadata?: Record<string, unknown>;
1764
1771
  }
1765
1772
 
1766
1773
  // ── processMessage ───────────────────────────────────────────────────
@@ -1785,6 +1792,8 @@ export async function processMessage(
1785
1792
  overrideProfile,
1786
1793
  displayContent,
1787
1794
  sourceActorPrincipalId,
1795
+ scripted,
1796
+ metadata: callerMetadata,
1788
1797
  } = options;
1789
1798
  await conversation.ensureActorScopedHistory();
1790
1799
  // Snapshot persona context at turn start so later tool turns can't pick up
@@ -2057,7 +2066,8 @@ export async function processMessage(
2057
2066
  conversation.emitActivityState("thinking", "context_compacting", {
2058
2067
  requestId,
2059
2068
  });
2060
- const result = await conversation.forceCompact();
2069
+ // Same sink the result card below streams on (see the drain branch).
2070
+ const result = await conversation.forceCompact(onEvent);
2061
2071
  const responseText = formatCompactResult(result);
2062
2072
 
2063
2073
  const assistantMsg = createAssistantMessage(responseText);
@@ -2196,6 +2206,8 @@ export async function processMessage(
2196
2206
  attachments,
2197
2207
  requestId,
2198
2208
  displayContent,
2209
+ scripted,
2210
+ ...(callerMetadata ? { metadata: callerMetadata } : {}),
2199
2211
  });
2200
2212
  publishConversationMessagesChanged(conversation.conversationId);
2201
2213
  } catch (err) {