@vellumai/assistant 0.11.8 → 0.11.9-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 (326) hide show
  1. package/ARCHITECTURE.md +2 -2
  2. package/Dockerfile +8 -48
  3. package/docker-entrypoint.sh +5 -1
  4. package/docker-kata-apt-env.sh +3 -0
  5. package/docker-kata-apt-shims.sh +127 -0
  6. package/docker-kata-apt-wrapper.sh +45 -0
  7. package/docker-kata-chroot-exec.sh +35 -0
  8. package/docker-kata-pip.sh +8 -2
  9. package/docs/architecture/memory.md +8 -4
  10. package/docs/guardian-request-flow.md +18 -14
  11. package/docs/trusted-contact-access.md +1 -7
  12. package/node_modules/@vellumai/avatar-catalog/src/catalog.ts +7 -1
  13. package/node_modules/@vellumai/avatar-catalog/src/index.ts +1 -1
  14. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/package.json +4 -1
  15. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/guardian-requests.ts +18 -0
  16. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  17. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/platform-credential.ts +41 -0
  18. package/node_modules/@vellumai/ces-client/node_modules/@vellumai/service-contracts/src/reactions.ts +60 -0
  19. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/package.json +4 -1
  20. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/guardian-requests.ts +18 -0
  21. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  22. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/platform-credential.ts +41 -0
  23. package/node_modules/@vellumai/gateway-client/node_modules/@vellumai/service-contracts/src/reactions.ts +60 -0
  24. package/node_modules/@vellumai/gateway-client/src/__tests__/guardian-request-contract.test.ts +0 -18
  25. package/node_modules/@vellumai/gateway-client/src/__tests__/inbound-event-kind.test.ts +110 -5
  26. package/node_modules/@vellumai/gateway-client/src/guardian-request-contract.ts +5 -33
  27. package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +3 -0
  28. package/node_modules/@vellumai/gateway-client/src/inbound-event-kind.ts +100 -9
  29. package/node_modules/@vellumai/gateway-client/src/index.ts +1 -2
  30. package/node_modules/@vellumai/gateway-client/src/outbound-contract.ts +9 -0
  31. package/node_modules/@vellumai/service-contracts/package.json +4 -1
  32. package/node_modules/@vellumai/service-contracts/src/guardian-requests.ts +18 -0
  33. package/node_modules/@vellumai/service-contracts/src/index.ts +1 -0
  34. package/node_modules/@vellumai/service-contracts/src/platform-credential.ts +41 -0
  35. package/node_modules/@vellumai/service-contracts/src/reactions.ts +60 -0
  36. package/openapi.yaml +193 -4
  37. package/package.json +2 -2
  38. package/src/__tests__/access-request-card-view.test.ts +6 -5
  39. package/src/__tests__/access-request-seed-content-blocks.test.ts +5 -2
  40. package/src/__tests__/agent-loop-mutable-latest-user-message.test.ts +43 -82
  41. package/src/__tests__/always-loaded-tools-guard.test.ts +8 -2
  42. package/src/__tests__/attachment-stored-path-annotation.test.ts +80 -0
  43. package/src/__tests__/attachments-store.test.ts +27 -0
  44. package/src/__tests__/attachments.test.ts +43 -0
  45. package/src/__tests__/canned-reply-release.test.ts +48 -5
  46. package/src/__tests__/channel-delivery-store.test.ts +129 -18
  47. package/src/__tests__/channel-reply-delivery.test.ts +553 -93
  48. package/src/__tests__/channel-retry-sweep.test.ts +199 -0
  49. package/src/__tests__/client-os-metadata-persistence.test.ts +32 -3
  50. package/src/__tests__/conversation-error.test.ts +15 -0
  51. package/src/__tests__/conversation-load-history-repair.test.ts +118 -0
  52. package/src/__tests__/conversation-pairing.test.ts +141 -1
  53. package/src/__tests__/conversation-routes-disk-view.test.ts +28 -1
  54. package/src/__tests__/conversation-routes-slash-commands.test.ts +125 -0
  55. package/src/__tests__/conversation-runtime-assembly.test.ts +79 -0
  56. package/src/__tests__/conversation-store-ephemeral.test.ts +323 -10
  57. package/src/__tests__/conversation-sync-tags.test.ts +2 -37
  58. package/src/__tests__/credential-health-service.test.ts +98 -10
  59. package/src/__tests__/delete-propagation.test.ts +469 -0
  60. package/src/__tests__/dm-persistence.test.ts +16 -0
  61. package/src/__tests__/docker-kata-apt-shims.test.ts +135 -0
  62. package/src/__tests__/forbidden-legacy-symbols.test.ts +12 -0
  63. package/src/__tests__/gemini-provider.test.ts +46 -0
  64. package/src/__tests__/guardian-card-withdrawal.test.ts +0 -22
  65. package/src/__tests__/guardian-gateway-sim.ts +0 -23
  66. package/src/__tests__/guardian-question-mode.test.ts +108 -0
  67. package/src/__tests__/guardian-reply-router-answer-mode.test.ts +0 -2
  68. package/src/__tests__/guardian-routing-invariants.test.ts +26 -12
  69. package/src/__tests__/host-proxy-interface.test.ts +10 -0
  70. package/src/__tests__/inbound-slack-persistence.test.ts +16 -0
  71. package/src/__tests__/injector-v3-suppression.test.ts +43 -9
  72. package/src/__tests__/list-messages-page-latest.test.ts +127 -0
  73. package/src/__tests__/list-messages-system-card.test.ts +103 -0
  74. package/src/__tests__/media-resolve-image-validation.test.ts +77 -0
  75. package/src/__tests__/notification-decision-fallback.test.ts +199 -77
  76. package/src/__tests__/notification-decision-strategy.test.ts +198 -213
  77. package/src/__tests__/notification-discord-adapter.test.ts +25 -0
  78. package/src/__tests__/notification-slack-adapter.test.ts +133 -0
  79. package/src/__tests__/notification-telegram-adapter.test.ts +36 -0
  80. package/src/__tests__/openai-provider.test.ts +5 -5
  81. package/src/__tests__/outbound-slack-persistence.test.ts +85 -72
  82. package/src/__tests__/persist-user-message-set-processing-failure.test.ts +83 -65
  83. package/src/__tests__/platform-client-verify-credential.test.ts +100 -0
  84. package/src/__tests__/plugin-import-boundary-guard.test.ts +1 -1
  85. package/src/__tests__/pricing.test.ts +11 -0
  86. package/src/__tests__/process-message-display-content.test.ts +20 -9
  87. package/src/__tests__/processing-acquire-fenced-guard.test.ts +56 -0
  88. package/src/__tests__/provider-meta-persistence.test.ts +67 -0
  89. package/src/__tests__/reaction-persistence.test.ts +22 -198
  90. package/src/__tests__/run-conversation-turn-persistence.test.ts +5 -2
  91. package/src/__tests__/scripted-turn-metadata-persistence.test.ts +16 -0
  92. package/src/__tests__/skill-load-tool.test.ts +27 -0
  93. package/src/__tests__/skills.test.ts +34 -1
  94. package/src/__tests__/strip-memory-injections.test.ts +3 -4
  95. package/src/__tests__/terminal-tools.test.ts +9 -0
  96. package/src/__tests__/thread-backfill.test.ts +5 -3
  97. package/src/__tests__/unified-turn-context-location.test.ts +76 -0
  98. package/src/__tests__/voice-session-bridge.test.ts +18 -0
  99. package/src/__tests__/watch-retro-report-payload.test.ts +185 -0
  100. package/src/__tests__/watch-retro-tool-availability.test.ts +82 -0
  101. package/src/__tests__/workspace-migration-151-repair-renamed-fireworks-deepseek-pro-model-id.test.ts +235 -0
  102. package/src/__tests__/workspace-migration-152-repair-retired-fireworks-minimax-m2p7-model-id.test.ts +233 -0
  103. package/src/agent/attachments.ts +13 -2
  104. package/src/agent/loop.ts +3 -19
  105. package/src/api/README.md +9 -5
  106. package/src/api/index.ts +9 -8
  107. package/src/api/package.json +1 -0
  108. package/src/api/responses/conversation-message.ts +8 -0
  109. package/src/api/responses/home.ts +7 -18
  110. package/src/api/surfaces.ts +114 -7
  111. package/src/approvals/AGENTS.md +1 -1
  112. package/src/channels/__tests__/gateway-guardian-requests.test.ts +1 -23
  113. package/src/channels/__tests__/types.test.ts +22 -1
  114. package/src/channels/gateway-guardian-requests.ts +0 -22
  115. package/src/channels/types.ts +8 -6
  116. package/src/cli/__tests__/catalog-search-help.test.ts +18 -0
  117. package/src/cli/commands/channels/__tests__/channels.test.ts +26 -0
  118. package/src/cli/commands/channels/index.help.ts +19 -6
  119. package/src/cli/commands/channels/index.ts +6 -2
  120. package/src/cli/commands/db/__tests__/status.test.ts +22 -0
  121. package/src/cli/commands/db/index.help.ts +1 -1
  122. package/src/cli/commands/db/status.ts +172 -1
  123. package/src/cli/commands/platform/__tests__/connect.test.ts +29 -0
  124. package/src/cli/commands/platform/connect.ts +14 -5
  125. package/src/cli/commands/plugins.help.ts +6 -5
  126. package/src/cli/lib/__tests__/install-from-github.test.ts +0 -8
  127. package/src/cli/lib/__tests__/install-from-platform.test.ts +72 -0
  128. package/src/cli/lib/__tests__/plugin-catalog-local.test.ts +33 -0
  129. package/src/cli/lib/bundled-marketplace.json +14 -0
  130. package/src/cli/lib/install-from-github.ts +9 -5
  131. package/src/cli/lib/install-from-platform.ts +12 -1
  132. package/src/config/__tests__/assistant-initiated-threads-gate.test.ts +61 -0
  133. package/src/config/assistant-initiated-threads-gate.ts +50 -0
  134. package/src/config/bundled-skills/acp/SKILL.md +10 -3
  135. package/src/config/bundled-skills/schedule/SKILL.md +25 -11
  136. package/src/config/bundled-skills/schedule/references/SCRIPT_MODE_PATTERNS.md +3 -1
  137. package/src/config/call-site-defaults.ts +4 -1
  138. package/src/config/feature-flag-registry.json +21 -4
  139. package/src/config/schemas/memory-v3.ts +4 -3
  140. package/src/context/strip-injections.ts +17 -56
  141. package/src/conversations/__tests__/message-consolidation.test.ts +39 -0
  142. package/src/conversations/message-consolidation.ts +4 -3
  143. package/src/credential-health/credential-health-service.ts +130 -0
  144. package/src/daemon/__tests__/conversation-tool-setup.test.ts +31 -0
  145. package/src/daemon/conversation-agent-loop-handlers.ts +55 -59
  146. package/src/daemon/conversation-error.ts +2 -0
  147. package/src/daemon/conversation-messaging.ts +195 -47
  148. package/src/daemon/conversation-process.ts +20 -5
  149. package/src/daemon/conversation-runtime-assembly.ts +48 -39
  150. package/src/daemon/conversation-store.ts +159 -11
  151. package/src/daemon/conversation-tool-setup.ts +9 -2
  152. package/src/daemon/conversation.ts +290 -19
  153. package/src/daemon/dictation-text-processing.ts +2 -7
  154. package/src/daemon/handlers/config-channels.ts +33 -3
  155. package/src/daemon/handlers/config-model.test.ts +1 -0
  156. package/src/daemon/handlers/shared.ts +9 -0
  157. package/src/daemon/port-oversized-content.test.ts +117 -0
  158. package/src/daemon/port-oversized-content.ts +110 -0
  159. package/src/daemon/process-message.ts +87 -16
  160. package/src/daemon/reaction-record.test.ts +23 -11
  161. package/src/daemon/reaction-record.ts +6 -9
  162. package/src/documents/document-store.ts +1 -5
  163. package/src/home/conversation-starter-validation.ts +1 -5
  164. package/src/live-voice/__tests__/live-voice-photo.test.ts +910 -50
  165. package/src/live-voice/__tests__/live-voice-sight-frame-inline.test.ts +31 -25
  166. package/src/live-voice/__tests__/live-voice-sight-frame.test.ts +70 -1
  167. package/src/live-voice/live-voice-photo.ts +544 -61
  168. package/src/live-voice/live-voice-session.ts +6 -2
  169. package/src/live-voice/protocol.ts +20 -1
  170. package/src/messaging/provider-message-metadata.ts +40 -1
  171. package/src/messaging/providers/__tests__/transport-dispatch.test.ts +10 -2
  172. package/src/messaging/providers/channel-transport.ts +28 -0
  173. package/src/messaging/providers/discord/send.ts +3 -2
  174. package/src/messaging/providers/slack/api.ts +11 -1
  175. package/src/messaging/providers/slack/message-metadata.test.ts +133 -0
  176. package/src/messaging/providers/slack/message-metadata.ts +141 -1
  177. package/src/messaging/providers/slack/send.test.ts +58 -0
  178. package/src/messaging/providers/slack/send.ts +4 -4
  179. package/src/messaging/providers/slack/transport.ts +28 -2
  180. package/src/messaging/providers/telegram-bot/send.test.ts +159 -0
  181. package/src/messaging/providers/telegram-bot/send.ts +93 -0
  182. package/src/messaging/providers/telegram-bot/transport.ts +71 -0
  183. package/src/messaging/reaction-envelopes.test.ts +73 -0
  184. package/src/messaging/reaction-envelopes.ts +33 -5
  185. package/src/messaging/read-provider-metadata.ts +4 -3
  186. package/src/monitoring/recovery/__tests__/stranded-delivery-events.test.ts +208 -0
  187. package/src/monitoring/recovery/db.ts +35 -0
  188. package/src/monitoring/recovery/orphaned-channel-events.ts +3 -22
  189. package/src/monitoring/recovery/run-recovery.ts +6 -2
  190. package/src/monitoring/recovery/stale-processing.ts +3 -20
  191. package/src/monitoring/recovery/stranded-delivery-events.ts +68 -0
  192. package/src/notifications/AGENTS.md +2 -2
  193. package/src/notifications/README.md +2 -2
  194. package/src/notifications/__tests__/assistant-reply-producer.test.ts +11 -9
  195. package/src/notifications/__tests__/broadcaster.test.ts +35 -4
  196. package/src/notifications/__tests__/notification-utils.test.ts +31 -0
  197. package/src/notifications/access-request-copy.ts +126 -114
  198. package/src/notifications/adapters/discord.ts +9 -5
  199. package/src/notifications/adapters/shared.ts +17 -4
  200. package/src/notifications/adapters/slack.ts +14 -4
  201. package/src/notifications/adapters/telegram.ts +8 -4
  202. package/src/notifications/approval-card-data.ts +5 -2
  203. package/src/notifications/assistant-reply-producer.ts +7 -7
  204. package/src/notifications/broadcaster.ts +57 -25
  205. package/src/notifications/conversation-pairing.ts +68 -2
  206. package/src/notifications/copy-composer.ts +13 -32
  207. package/src/notifications/decision-engine.ts +56 -237
  208. package/src/notifications/guardian-delivery-recorder.ts +5 -9
  209. package/src/notifications/guardian-feed-projection.ts +0 -15
  210. package/src/notifications/guardian-question-mode.ts +147 -6
  211. package/src/notifications/notification-utils.ts +117 -1
  212. package/src/permissions/confirmation-guardian-request.ts +2 -2
  213. package/src/permissions/prompter.ts +1 -1
  214. package/src/persistence/conversation-crud.ts +93 -14
  215. package/src/persistence/conversation-queries.ts +144 -17
  216. package/src/persistence/conversation-types.ts +39 -5
  217. package/src/persistence/delivery-crud.ts +224 -28
  218. package/src/persistence/delivery-status.ts +21 -2
  219. package/src/persistence/migrations/374-channel-inbound-message-id-index.ts +26 -0
  220. package/src/persistence/migrations/375-create-channel-outbound-posts.ts +52 -0
  221. package/src/persistence/migrations/__tests__/375-create-channel-outbound-posts.test.ts +84 -0
  222. package/src/persistence/schema/conversations.ts +71 -2
  223. package/src/persistence/schema-contract.test.ts +78 -0
  224. package/src/persistence/schema-contract.ts +96 -0
  225. package/src/persistence/steps.ts +4 -0
  226. package/src/platform/client.ts +55 -0
  227. package/src/plugins/__tests__/mcp-servers.test.ts +46 -0
  228. package/src/plugins/defaults/injector-order.ts +2 -1
  229. package/src/plugins/defaults/memory/__tests__/memory-retrospective-accounting.test.ts +2 -2
  230. package/src/plugins/defaults/memory/context-search/sources/conversations.ts +13 -15
  231. package/src/plugins/defaults/memory/hooks/user-prompt-submit.ts +10 -0
  232. package/src/plugins/defaults/memory/injectors.ts +1 -1
  233. package/src/plugins/defaults/memory/memory-marker.ts +17 -7
  234. package/src/plugins/defaults/memory/substrate/__tests__/consolidation-prompt-flag-gating-guard.test.ts +1 -5
  235. package/src/plugins/defaults/memory/substrate/__tests__/skill-content.test.ts +17 -0
  236. package/src/plugins/defaults/memory/substrate/skill-content.ts +7 -0
  237. package/src/plugins/defaults/memory/v3/__tests__/carry-integration.test.ts +54 -41
  238. package/src/plugins/defaults/memory/v3/__tests__/injection.test.ts +3 -3
  239. package/src/plugins/defaults/memory/v3/__tests__/render-injection.test.ts +22 -0
  240. package/src/plugins/defaults/memory/v3/injector.ts +14 -15
  241. package/src/plugins/defaults/memory/v3/prune.test.ts +20 -2
  242. package/src/plugins/defaults/memory/v3/render-injection.ts +10 -1
  243. package/src/plugins/defaults/memory/v3/types.ts +15 -4
  244. package/src/plugins/defaults/turn-context/injectors.ts +3 -0
  245. package/src/plugins/defaults/turn-context/unified-turn-context.ts +24 -0
  246. package/src/plugins/mcp-servers.ts +14 -2
  247. package/src/providers/__tests__/dispatch-connection-routing.test.ts +50 -0
  248. package/src/providers/__tests__/registry-native-web-search.test.ts +49 -2
  249. package/src/providers/anthropic/client.ts +21 -24
  250. package/src/providers/call-site-routing.ts +5 -4
  251. package/src/providers/connection-resolution.ts +29 -1
  252. package/src/providers/content-block-size.test.ts +82 -0
  253. package/src/providers/content-block-size.ts +99 -0
  254. package/src/providers/file-block-text.test.ts +64 -0
  255. package/src/providers/file-block-text.ts +28 -0
  256. package/src/providers/gemini/client.ts +15 -9
  257. package/src/providers/inference/auth.ts +6 -6
  258. package/src/providers/media-resolve.ts +14 -0
  259. package/src/providers/model-catalog.ts +47 -34
  260. package/src/providers/openai/chat-completions-provider.ts +9 -25
  261. package/src/providers/openai/responses-provider.ts +11 -18
  262. package/src/providers/registry.ts +10 -7
  263. package/src/providers/routing-identity.ts +2 -1
  264. package/src/providers/types.ts +1 -4
  265. package/src/providers/vellum-model-routing.ts +3 -2
  266. package/src/runtime/AGENTS.md +40 -15
  267. package/src/runtime/approval-message-composer.ts +1 -6
  268. package/src/runtime/assistant-event-hub.ts +2 -39
  269. package/src/runtime/channel-approval-types.ts +1 -5
  270. package/src/runtime/channel-reply-delivery.ts +182 -115
  271. package/src/runtime/{slack-reply-session.test.ts → channel-reply-session.test.ts} +294 -156
  272. package/src/runtime/{slack-reply-session.ts → channel-reply-session.ts} +107 -86
  273. package/src/runtime/channel-retry-sweep.ts +102 -1
  274. package/src/runtime/finalize-event-delivery.ts +4 -4
  275. package/src/runtime/guardian-action-message-composer.ts +1 -4
  276. package/src/runtime/guardian-reply-router.ts +4 -75
  277. package/src/runtime/http-router.ts +1 -5
  278. package/src/runtime/question-request-guardian-bridge.ts +2 -3
  279. package/src/runtime/routes/__tests__/conversation-list-assistant-section.test.ts +359 -0
  280. package/src/runtime/routes/__tests__/dictation-command-mode.test.ts +120 -0
  281. package/src/runtime/routes/__tests__/sight-frame-routes.test.ts +487 -0
  282. package/src/runtime/routes/acp-routes.ts +1 -1
  283. package/src/runtime/routes/canned-reply-release.ts +19 -9
  284. package/src/runtime/routes/channel-route-shared.ts +0 -39
  285. package/src/runtime/routes/channel-verification-routes.ts +4 -1
  286. package/src/runtime/routes/conversation-list-routes.ts +44 -3
  287. package/src/runtime/routes/conversation-management-routes.ts +2 -1
  288. package/src/runtime/routes/conversation-routes.ts +138 -59
  289. package/src/runtime/routes/diagnostics-routes.ts +111 -60
  290. package/src/runtime/routes/guardian-action-routes.ts +10 -14
  291. package/src/runtime/routes/guardian-approval-interception.ts +0 -5
  292. package/src/runtime/routes/inbound-message-handler.ts +170 -48
  293. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +4 -3
  294. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +169 -8
  295. package/src/runtime/routes/inbound-stages/background-dispatch.ts +82 -14
  296. package/src/runtime/routes/inbound-stages/guardian-reply-intercept.ts +23 -48
  297. package/src/runtime/routes/inbound-stages/reaction-intercept.test.ts +454 -94
  298. package/src/runtime/routes/inbound-stages/reaction-intercept.ts +309 -102
  299. package/src/runtime/routes/index.ts +2 -0
  300. package/src/runtime/routes/platform-routes.ts +99 -10
  301. package/src/runtime/routes/sight-frame-routes.ts +136 -0
  302. package/src/runtime/sync/resource-sync-events.ts +14 -37
  303. package/src/runtime/sync/sync-publisher.test.ts +5 -4
  304. package/src/tools/__tests__/tool-input-schemas.test.ts +2 -0
  305. package/src/tools/client-os.ts +11 -1
  306. package/src/tools/host-filesystem/edit.ts +2 -2
  307. package/src/tools/host-filesystem/read.ts +2 -2
  308. package/src/tools/host-filesystem/transfer.ts +2 -2
  309. package/src/tools/host-filesystem/write.ts +2 -2
  310. package/src/tools/host-terminal/host-shell.ts +2 -2
  311. package/src/tools/skills/load.ts +1 -1
  312. package/src/tools/terminal/safe-env.ts +4 -0
  313. package/src/tools/tool-input-schemas.ts +2 -0
  314. package/src/tools/tool-manifest.ts +2 -0
  315. package/src/tools/ui-surface/definitions.ts +4 -3
  316. package/src/tools/watch/watch-retro-report.ts +205 -0
  317. package/src/watch/__tests__/watch-retro.test.ts +268 -40
  318. package/src/watch/watch-retro.ts +190 -77
  319. package/src/workspace/migrations/151-repair-renamed-fireworks-deepseek-pro-model-id.ts +195 -0
  320. package/src/workspace/migrations/152-repair-retired-fireworks-minimax-m2p7-model-id.ts +198 -0
  321. package/src/workspace/migrations/__tests__/150-stt-flux-provider-to-model-family.test.ts +0 -10
  322. package/src/workspace/migrations/registry.ts +4 -0
  323. package/docker-kata-pip-chroot.sh +0 -22
  324. package/src/__tests__/slack-reaction-approvals.test.ts +0 -97
  325. package/src/__tests__/slack-reaction-guardian-approval.test.ts +0 -307
  326. package/src/api/events/conversation-list-invalidated.ts +0 -38
@@ -0,0 +1,233 @@
1
+ import {
2
+ existsSync,
3
+ mkdirSync,
4
+ readFileSync,
5
+ rmSync,
6
+ writeFileSync,
7
+ } from "node:fs";
8
+ import { tmpdir } from "node:os";
9
+ import { join } from "node:path";
10
+ import { Database } from "bun:sqlite";
11
+ import { afterEach, beforeEach, describe, expect, test } from "bun:test";
12
+
13
+ import { repairRetiredFireworksMinimaxM2p7ModelIdMigration } from "../workspace/migrations/152-repair-retired-fireworks-minimax-m2p7-model-id.js";
14
+ import { WORKSPACE_MIGRATIONS } from "../workspace/migrations/registry.js";
15
+ import { assertNotLiveDb } from "./assert-not-live-db.js";
16
+
17
+ const STALE = "accounts/fireworks/models/minimax-m2p7";
18
+ const REPLACEMENT = "accounts/fireworks/models/minimax-m3";
19
+
20
+ let workspaceDir: string;
21
+
22
+ function freshWorkspace(): void {
23
+ workspaceDir = join(
24
+ tmpdir(),
25
+ `vellum-migration-152-test-${Date.now()}-${Math.random().toString(36).slice(2)}`,
26
+ );
27
+ mkdirSync(workspaceDir, { recursive: true });
28
+ }
29
+
30
+ function writeConfig(data: Record<string, unknown>): void {
31
+ writeFileSync(
32
+ join(workspaceDir, "config.json"),
33
+ JSON.stringify(data, null, 2) + "\n",
34
+ );
35
+ }
36
+
37
+ function readConfig(): Record<string, unknown> {
38
+ return JSON.parse(readFileSync(join(workspaceDir, "config.json"), "utf-8"));
39
+ }
40
+
41
+ function seedRows(rows: Array<{ name: string; provider: string }>): void {
42
+ mkdirSync(join(workspaceDir, "data", "db"), { recursive: true });
43
+ const db = new Database(join(workspaceDir, "data", "db", "assistant.db"));
44
+ db.run(`CREATE TABLE IF NOT EXISTS provider_connections (
45
+ name TEXT PRIMARY KEY,
46
+ provider TEXT NOT NULL,
47
+ auth TEXT NOT NULL,
48
+ created_at INTEGER NOT NULL,
49
+ updated_at INTEGER NOT NULL
50
+ )`);
51
+ for (const row of rows) {
52
+ db.query(
53
+ `INSERT INTO provider_connections (name, provider, auth, created_at, updated_at) VALUES (?, ?, '{"type":"api_key"}', 1, 1)`,
54
+ ).run(row.name, row.provider);
55
+ }
56
+ db.close();
57
+ }
58
+
59
+ beforeEach(() => {
60
+ freshWorkspace();
61
+ });
62
+
63
+ afterEach(() => {
64
+ if (existsSync(workspaceDir)) {
65
+ assertNotLiveDb(workspaceDir);
66
+ rmSync(workspaceDir, { recursive: true, force: true });
67
+ }
68
+ });
69
+
70
+ describe("152-repair-retired-fireworks-minimax-m2p7-model-id migration", () => {
71
+ test("has correct migration id and is registered", () => {
72
+ expect(repairRetiredFireworksMinimaxM2p7ModelIdMigration.id).toBe(
73
+ "152-repair-retired-fireworks-minimax-m2p7-model-id",
74
+ );
75
+ expect(WORKSPACE_MIGRATIONS.map((m) => m.id)).toContain(
76
+ "152-repair-retired-fireworks-minimax-m2p7-model-id",
77
+ );
78
+ });
79
+
80
+ test("repairs the stale ID in default, call sites, and profiles", () => {
81
+ writeConfig({
82
+ llm: {
83
+ default: { provider: "fireworks", model: STALE },
84
+ callSites: {
85
+ recall: { model: STALE, maxTokens: 4096 },
86
+ heartbeat: { model: `${STALE}-tuned` },
87
+ malformed: STALE,
88
+ },
89
+ profiles: {
90
+ "cost-optimized": { provider: "vellum", model: STALE },
91
+ legacy: { model: STALE },
92
+ },
93
+ },
94
+ });
95
+
96
+ repairRetiredFireworksMinimaxM2p7ModelIdMigration.run(workspaceDir);
97
+
98
+ const llm = readConfig().llm as Record<string, any>;
99
+ expect(llm.default.model).toBe(REPLACEMENT);
100
+ expect(llm.callSites.recall.model).toBe(REPLACEMENT);
101
+ expect(llm.callSites.recall.maxTokens).toBe(4096);
102
+ // Non-exact matches and malformed leaves are untouched.
103
+ expect(llm.callSites.heartbeat.model).toBe(`${STALE}-tuned`);
104
+ expect(llm.callSites.malformed).toBe(STALE);
105
+ // Managed profiles stamped provider "vellum" carry Fireworks model IDs.
106
+ expect(llm.profiles["cost-optimized"].model).toBe(REPLACEMENT);
107
+ expect(llm.profiles.legacy.model).toBe(REPLACEMENT);
108
+ });
109
+
110
+ test("repairs entry-bound fragments whose row kind is fireworks or vellum", () => {
111
+ seedRows([
112
+ { name: "my-fireworks", provider: "fireworks" },
113
+ { name: "managed-alt", provider: "vellum" },
114
+ ]);
115
+ writeConfig({
116
+ llm: {
117
+ profiles: {
118
+ bound: { provider: "my-fireworks", model: STALE },
119
+ managed: { provider: "managed-alt", model: STALE },
120
+ },
121
+ },
122
+ });
123
+
124
+ repairRetiredFireworksMinimaxM2p7ModelIdMigration.run(workspaceDir);
125
+
126
+ const llm = readConfig().llm as Record<string, any>;
127
+ expect(llm.profiles.bound.model).toBe(REPLACEMENT);
128
+ expect(llm.profiles.managed.model).toBe(REPLACEMENT);
129
+ });
130
+
131
+ test("leaves fragments with an explicit other provider untouched", () => {
132
+ seedRows([{ name: "byo-compat", provider: "openai-compatible" }]);
133
+ writeConfig({
134
+ llm: {
135
+ default: { provider: "openai-compatible", model: STALE },
136
+ profiles: {
137
+ byo: { provider: "openai-compatible", model: STALE },
138
+ // Entry-bound profile whose row kind is not repairable.
139
+ compatBound: { provider: "byo-compat", model: STALE },
140
+ // Entry name with no row: dangling, so nothing proves it is a
141
+ // fireworks-kind route.
142
+ dangling: { provider: "ghost", model: STALE },
143
+ },
144
+ },
145
+ });
146
+
147
+ repairRetiredFireworksMinimaxM2p7ModelIdMigration.run(workspaceDir);
148
+
149
+ const llm = readConfig().llm as Record<string, any>;
150
+ expect(llm.default.model).toBe(STALE);
151
+ expect(llm.profiles.byo.model).toBe(STALE);
152
+ expect(llm.profiles.compatBound.model).toBe(STALE);
153
+ expect(llm.profiles.dangling.model).toBe(STALE);
154
+ });
155
+
156
+ test("leaves entry-name providers untouched when no DB file exists", () => {
157
+ writeConfig({
158
+ llm: {
159
+ profiles: { bound: { provider: "my-fireworks", model: STALE } },
160
+ },
161
+ });
162
+
163
+ repairRetiredFireworksMinimaxM2p7ModelIdMigration.run(workspaceDir);
164
+
165
+ const llm = readConfig().llm as Record<string, any>;
166
+ expect(llm.profiles.bound.model).toBe(STALE);
167
+ });
168
+
169
+ test("throws when an entry-name provider needs rows and the DB is unreadable", () => {
170
+ // A DB file without the provider_connections table is unqueryable, so
171
+ // the run must fail (and retry later) instead of skipping the profile.
172
+ mkdirSync(join(workspaceDir, "data", "db"), { recursive: true });
173
+ writeFileSync(join(workspaceDir, "data", "db", "assistant.db"), "");
174
+ writeConfig({
175
+ llm: {
176
+ profiles: { bound: { provider: "my-fireworks", model: STALE } },
177
+ },
178
+ });
179
+
180
+ expect(() =>
181
+ repairRetiredFireworksMinimaxM2p7ModelIdMigration.run(workspaceDir),
182
+ ).toThrow();
183
+ const llm = readConfig().llm as Record<string, any>;
184
+ expect(llm.profiles.bound.model).toBe(STALE);
185
+
186
+ // Vendor and absent providers never need the rows, so the same broken
187
+ // DB does not block their repair.
188
+ writeConfig({
189
+ llm: {
190
+ default: { provider: "fireworks", model: STALE },
191
+ profiles: { legacy: { model: STALE } },
192
+ },
193
+ });
194
+ repairRetiredFireworksMinimaxM2p7ModelIdMigration.run(workspaceDir);
195
+ const repaired = readConfig().llm as Record<string, any>;
196
+ expect(repaired.default.model).toBe(REPLACEMENT);
197
+ expect(repaired.profiles.legacy.model).toBe(REPLACEMENT);
198
+ });
199
+
200
+ test("is idempotent and a no-op without the stale ID", () => {
201
+ writeConfig({
202
+ llm: {
203
+ default: { provider: "fireworks", model: STALE },
204
+ profiles: { fine: { provider: "fireworks", model: REPLACEMENT } },
205
+ },
206
+ });
207
+
208
+ repairRetiredFireworksMinimaxM2p7ModelIdMigration.run(workspaceDir);
209
+ const first = readConfig();
210
+ repairRetiredFireworksMinimaxM2p7ModelIdMigration.run(workspaceDir);
211
+ expect(readConfig()).toEqual(first);
212
+
213
+ const llm = first.llm as Record<string, any>;
214
+ expect(llm.default.model).toBe(REPLACEMENT);
215
+ expect(llm.profiles.fine.model).toBe(REPLACEMENT);
216
+ });
217
+
218
+ test("handles missing config, missing llm block, and invalid JSON", () => {
219
+ // No config.json at all.
220
+ expect(() =>
221
+ repairRetiredFireworksMinimaxM2p7ModelIdMigration.run(workspaceDir),
222
+ ).not.toThrow();
223
+
224
+ writeConfig({ theme: "dark" });
225
+ repairRetiredFireworksMinimaxM2p7ModelIdMigration.run(workspaceDir);
226
+ expect(readConfig()).toEqual({ theme: "dark" });
227
+
228
+ writeFileSync(join(workspaceDir, "config.json"), "{not json");
229
+ expect(() =>
230
+ repairRetiredFireworksMinimaxM2p7ModelIdMigration.run(workspaceDir),
231
+ ).not.toThrow();
232
+ });
233
+ });
@@ -1,3 +1,4 @@
1
+ import { extractedTextForFileBlock } from "../providers/content-block-size.js";
1
2
  import {
2
3
  attachmentIdFragment,
3
4
  type ContentBlock,
@@ -64,6 +65,10 @@ export function attachmentsToReferenceBlocks(
64
65
  } as ContentBlock;
65
66
  }
66
67
 
68
+ const extractedText = extractedTextForFileBlock(
69
+ ref.mimeType,
70
+ ref.extractedText,
71
+ );
67
72
  return {
68
73
  type: "file",
69
74
  source: {
@@ -73,7 +78,7 @@ export function attachmentsToReferenceBlocks(
73
78
  sizeBytes: ref.sizeBytes,
74
79
  filename: ref.filename,
75
80
  },
76
- extracted_text: ref.extractedText,
81
+ ...(extractedText !== undefined ? { extracted_text: extractedText } : {}),
77
82
  } as ContentBlock;
78
83
  });
79
84
  }
@@ -99,6 +104,10 @@ export function attachmentsToContentBlocks(
99
104
  } as ContentBlock;
100
105
  }
101
106
 
107
+ const extractedText = extractedTextForFileBlock(
108
+ attachment.mimeType,
109
+ attachment.extractedText,
110
+ );
102
111
  return {
103
112
  type: "file",
104
113
  source: {
@@ -107,7 +116,9 @@ export function attachmentsToContentBlocks(
107
116
  data: attachment.data,
108
117
  filename: attachment.filename,
109
118
  },
110
- extracted_text: attachment.extractedText,
119
+ ...(extractedText !== undefined
120
+ ? { extracted_text: extractedText }
121
+ : {}),
111
122
  ...attachmentIdFragment(attachment.id),
112
123
  } as ContentBlock;
113
124
  }),
package/src/agent/loop.ts CHANGED
@@ -2,7 +2,6 @@ import type { AnsweredQuestion } from "../api/events/question-answered.js";
2
2
  import type { LLMCallSite } from "../config/schemas/llm.js";
3
3
  import { recordEstimate } from "../context/estimator-calibration.js";
4
4
  import { preModelCallSanitize } from "../context/outbound-sanitize.js";
5
- import { turnStartUserMessageHasSpotlight } from "../context/strip-injections.js";
6
5
  import {
7
6
  estimatePromptTokensRaw,
8
7
  estimatePromptTokensWithTools,
@@ -1551,21 +1550,6 @@ export class AgentLoop {
1551
1550
  providerConfig.cacheTtl = this.config.cacheTtl;
1552
1551
  }
1553
1552
 
1554
- // Cache-anchor signal for turns whose opening message is volatile. The
1555
- // memory-v3 `<memory_spotlight>` block is the only injected block that
1556
- // is strip-and-replaced from every user message each turn, so when it
1557
- // is present that message's bytes do not recur next turn and a
1558
- // long-TTL breakpoint on it could never be read back. The provider
1559
- // marks it at the short TTL instead. Derived from the history actually
1560
- // being sent rather than from configuration, so turns where memory
1561
- // contributed no spotlight keep a normal anchor. Read off the
1562
- // turn-starting message, so the signal holds for every request in the
1563
- // turn rather than flipping once tool results arrive. Only set when
1564
- // true so the wire/config stays byte-identical when absent.
1565
- if (turnStartUserMessageHasSpotlight(history)) {
1566
- providerConfig.mutableLatestUserMessage = true;
1567
- }
1568
-
1569
1553
  // Per-call LLM call-site identifier. Surfaces on the per-call
1570
1554
  // `config.callSite` so `RetryProvider.normalizeSendMessageOptions`
1571
1555
  // can route through `resolveCallSiteConfig` against
@@ -1652,7 +1636,7 @@ export class AgentLoop {
1652
1636
  // Sanitize the outbound history right before sending: drop accumulated
1653
1637
  // media, collapse old AX-tree snapshots, and convert historical
1654
1638
  // web-search results to text. See {@link preModelCallSanitize}.
1655
- const providerHistory = timeSyncSection(
1639
+ const sanitizedHistory = timeSyncSection(
1656
1640
  "agent-loop:pre-model-call-sanitize",
1657
1641
  () => preModelCallSanitize(history),
1658
1642
  (sanitized) => ({ messageCount: sanitized.length }),
@@ -1837,7 +1821,7 @@ export class AgentLoop {
1837
1821
  let response: ProviderResponse;
1838
1822
  try {
1839
1823
  response = await traceAsyncSection("agent-loop:provider-send", () =>
1840
- this.provider.sendMessage(providerHistory, providerOptions),
1824
+ this.provider.sendMessage(sanitizedHistory, providerOptions),
1841
1825
  );
1842
1826
  } catch (llmCallError) {
1843
1827
  // Skip recording on abort — the user cancelled the request and
@@ -1855,7 +1839,7 @@ export class AgentLoop {
1855
1839
  // misrepresent both.
1856
1840
  const rawRequest = {
1857
1841
  provider: this.provider.name,
1858
- messages: providerHistory,
1842
+ messages: sanitizedHistory,
1859
1843
  tools: providerOptions.tools,
1860
1844
  systemPrompt: providerOptions.systemPrompt,
1861
1845
  config: providerOptions.config,
package/src/api/README.md CHANGED
@@ -5,9 +5,12 @@ evals, future external clients. This directory is the **source of truth** for
5
5
  the wire contracts the assistant exposes: schemas, types, and pure helpers.
6
6
 
7
7
  Internal assistant code imports the files in this directory via relative paths
8
- (e.g. `../../api/events/open-url.js`). External consumers import the
9
- materialized npm-style package `@vellumai/assistant-api`, regenerated into
10
- `clients/web/node_modules/` by `clients/web/scripts/postinstall.ts`.
8
+ (e.g. `../../api/events/open-url.js`). External consumers import it as the
9
+ workspace package `@vellumai/assistant-api`, a `workspace:*` symlink to this
10
+ directory, so an edit here reaches them immediately. The generated HTTP
11
+ client is separate: it comes from `assistant/openapi.yaml`, regenerated with
12
+ `cd assistant && bun run generate:openapi` and consumed by
13
+ `cd clients/web && bun run openapi-ts`.
11
14
 
12
15
  ## Architecture
13
16
 
@@ -110,8 +113,9 @@ Handler-level tests in the consuming domain modules typically need no change
110
113
  Run before push, in order:
111
114
 
112
115
  ```bash
113
- # In clients/web — regenerate the @vellumai/assistant-api bundle
114
- bun run scripts/postinstall.ts
116
+ # Regenerate the OpenAPI spec and the web client from it
117
+ ( cd assistant && bun run generate:openapi )
118
+ ( cd clients/web && bun run openapi-ts )
115
119
 
116
120
  # Type-check both packages
117
121
  ( cd assistant && bunx tsc --noEmit )
package/src/api/index.ts CHANGED
@@ -31,7 +31,6 @@ import { ContextCompactedEventSchema } from "./events/context-compacted.js";
31
31
  import { ContextWindowUsageEventSchema } from "./events/context-window-usage.js";
32
32
  import { ConversationErrorEventSchema } from "./events/conversation-error.js";
33
33
  import { ConversationInferenceProfileUpdatedEventSchema } from "./events/conversation-inference-profile-updated.js";
34
- import { ConversationListInvalidatedEventSchema } from "./events/conversation-list-invalidated.js";
35
34
  import { ConversationNoticeEventSchema } from "./events/conversation-notice.js";
36
35
  import { ConversationTitleUpdatedEventSchema } from "./events/conversation-title-updated.js";
37
36
  import { DiskPressureStatusChangedEventSchema } from "./events/disk-pressure-status-changed.js";
@@ -321,12 +320,6 @@ export {
321
320
  type ConversationInferenceProfileUpdatedEvent,
322
321
  ConversationInferenceProfileUpdatedEventSchema,
323
322
  } from "./events/conversation-inference-profile-updated.js";
324
- export {
325
- type ConversationListInvalidatedEvent,
326
- ConversationListInvalidatedEventSchema,
327
- type ConversationListInvalidatedReason,
328
- ConversationListInvalidatedReasonSchema,
329
- } from "./events/conversation-list-invalidated.js";
330
323
  export {
331
324
  type ConversationNoticeEvent,
332
325
  ConversationNoticeEventSchema,
@@ -913,6 +906,15 @@ export {
913
906
  TableSurfaceDataSchema,
914
907
  type VisualSurfaceData,
915
908
  VisualSurfaceDataSchema,
909
+ WATCH_RETRO_MAX_QUESTIONS,
910
+ type WatchRetroOption,
911
+ WatchRetroOptionSchema,
912
+ type WatchRetroQuestion,
913
+ type WatchRetroQuestionKind,
914
+ WatchRetroQuestionKindSchema,
915
+ WatchRetroQuestionSchema,
916
+ type WatchRetroSurfaceData,
917
+ WatchRetroSurfaceDataSchema,
916
918
  type WorkResultDiff,
917
919
  WorkResultDiffSchema,
918
920
  type WorkResultItem,
@@ -978,7 +980,6 @@ export const AssistantEventSchema = z.discriminatedUnion("type", [
978
980
  ContextWindowUsageEventSchema,
979
981
  ConversationErrorEventSchema,
980
982
  ConversationInferenceProfileUpdatedEventSchema,
981
- ConversationListInvalidatedEventSchema,
982
983
  ConversationNoticeEventSchema,
983
984
  ConversationTitleUpdatedEventSchema,
984
985
  DiskPressureStatusChangedEventSchema,
@@ -8,6 +8,7 @@
8
8
  ".": "./index.ts"
9
9
  },
10
10
  "dependencies": {
11
+ "@vellumai/service-contracts": "workspace:*",
11
12
  "zod": "4.3.6"
12
13
  }
13
14
  }
@@ -22,6 +22,7 @@
22
22
  * gateway, evals) import via `@vellumai/assistant-api`.
23
23
  */
24
24
 
25
+ import { ReactionEmojiFieldsSchema } from "@vellumai/service-contracts/reactions";
25
26
  import { z } from "zod";
26
27
 
27
28
  import {
@@ -358,6 +359,7 @@ const SlackMessageLinkSchema = z.object({
358
359
 
359
360
  const SlackReactionSchema = z.object({
360
361
  emoji: z.string(),
362
+ ...ReactionEmojiFieldsSchema.shape,
361
363
  op: z.enum(["added", "removed"]),
362
364
  actorDisplayName: z.string().optional(),
363
365
  targetChannelTs: z.string(),
@@ -621,6 +623,7 @@ export const ConversationMessageSchema = z.object({
621
623
  reaction: z
622
624
  .object({
623
625
  emoji: z.string(),
626
+ ...ReactionEmojiFieldsSchema.shape,
624
627
  op: z.enum(["added", "removed"]),
625
628
  targetMessageId: z.string(),
626
629
  actorDisplayName: z.string().optional(),
@@ -639,6 +642,11 @@ export const ConversationMessageSchema = z.object({
639
642
  })
640
643
  .optional(),
641
644
  slackMessage: ConversationSlackMessageSchema.optional(),
645
+ /** Unix ms at which the message was deleted on its channel after the daemon
646
+ * stored it (a Slack or Discord deletion the gateway forwarded). The stored
647
+ * content stays for audit and the Inspect view; clients render a tombstone
648
+ * in place of the content, mirroring what the channel now shows. */
649
+ deletedAt: z.number().optional(),
642
650
  /**
643
651
  * Queue state for a user message that is still waiting in the daemon's
644
652
  * in-memory queue (enqueued while the agent was mid-turn, not yet drained or
@@ -17,6 +17,10 @@
17
17
  * (web client, gateway, evals) import via `@vellumai/assistant-api`.
18
18
  */
19
19
 
20
+ import {
21
+ type GuardianRequestStatus,
22
+ GuardianRequestStatusSchema,
23
+ } from "@vellumai/service-contracts/guardian-requests";
20
24
  import { z } from "zod";
21
25
 
22
26
  // ---------------------------------------------------------------------------
@@ -107,24 +111,9 @@ export const FeedItemDetailPanelSchema = z.object({
107
111
  });
108
112
  export type FeedItemDetailPanel = z.infer<typeof FeedItemDetailPanelSchema>;
109
113
 
110
- /**
111
- * Canonical guardian-request status projected onto a feed item.
112
- *
113
- * Values mirror `GuardianRequestStatusSchema` in
114
- * `@vellumai/gateway-client` (this file is copied verbatim into client
115
- * packages, so it cannot import the contract; the daemon's projection
116
- * writer asserts the two stay aligned at compile time).
117
- */
118
- export const FeedItemGuardianStatusSchema = z.enum([
119
- "pending",
120
- "approved",
121
- "denied",
122
- "expired",
123
- "cancelled",
124
- ]);
125
- export type FeedItemGuardianStatus = z.infer<
126
- typeof FeedItemGuardianStatusSchema
127
- >;
114
+ /** Canonical guardian-request status projected onto a feed item. */
115
+ export const FeedItemGuardianStatusSchema = GuardianRequestStatusSchema;
116
+ export type FeedItemGuardianStatus = GuardianRequestStatus;
128
117
 
129
118
  /** Whether the guardian is being asked to approve or to answer. */
130
119
  export const FeedItemGuardianIntentSchema = z.enum(["approval", "question"]);
@@ -86,6 +86,23 @@ export function describeSurfaceDataStringParseFailure(
86
86
  }
87
87
  }
88
88
 
89
+ /**
90
+ * Required string that coerces a scalar, treating an absent field as blank.
91
+ *
92
+ * `z.coerce.string()` is not a validator on a missing field: it stringifies
93
+ * whatever it is handed, so `undefined` parses successfully as the literal
94
+ * "undefined" and a trailing `.catch()` never runs. That string then renders,
95
+ * and a field the model simply failed to send reads to the user as a word
96
+ * nobody wrote. A blank is what the callers already handle: an empty title
97
+ * falls back to a default, and an empty prompt or label is what the watch
98
+ * retro card filters an unanswerable question out on.
99
+ */
100
+ const coercedString = () =>
101
+ z.preprocess(
102
+ (value) => (value === undefined || value === null ? "" : value),
103
+ z.coerce.string().catch(""),
104
+ );
105
+
89
106
  /** Optional string that drops (rather than rejects on) a non-string value. */
90
107
  const tolerantString = () => z.string().optional().catch(undefined);
91
108
 
@@ -111,7 +128,7 @@ export const CardSurfaceDataSchema = z.object({
111
128
  subtitle: z.string().optional(),
112
129
  body: z.string().optional(),
113
130
  metadata: z
114
- .array(z.object({ label: z.coerce.string(), value: z.coerce.string() }))
131
+ .array(z.object({ label: coercedString(), value: coercedString() }))
115
132
  .optional(),
116
133
  /** Optional template name for specialized rendering (e.g. "weather_forecast"). */
117
134
  template: z.string().optional(),
@@ -298,7 +315,7 @@ export const FormFieldSchema = z.object({
298
315
  .optional()
299
316
  .catch(undefined),
300
317
  options: z
301
- .array(z.object({ label: z.coerce.string(), value: z.coerce.string() }))
318
+ .array(z.object({ label: coercedString(), value: coercedString() }))
302
319
  .optional()
303
320
  .catch(undefined),
304
321
  });
@@ -353,7 +370,7 @@ export const TableColumnSchema = z.object({
353
370
  export type TableColumn = z.infer<typeof TableColumnSchema>;
354
371
 
355
372
  export const TableCellValueSchema = z.object({
356
- text: z.coerce.string().catch(""),
373
+ text: coercedString(),
357
374
  /** SF Symbol name. */
358
375
  icon: tolerantString(),
359
376
  /** Semantic token: "success" | "warning" | "error" | "muted". */
@@ -402,7 +419,7 @@ export const DynamicPagePreviewSchema = z.object({
402
419
  description: tolerantString(),
403
420
  icon: tolerantString(),
404
421
  metrics: z
405
- .array(z.object({ label: z.coerce.string(), value: z.coerce.string() }))
422
+ .array(z.object({ label: coercedString(), value: coercedString() }))
406
423
  .optional()
407
424
  .catch(undefined),
408
425
  context: z.enum(["app_create", "general"]).optional().catch(undefined),
@@ -467,7 +484,7 @@ export const WorkResultSectionTypeSchema = z.enum([
467
484
  export type WorkResultSectionType = z.infer<typeof WorkResultSectionTypeSchema>;
468
485
 
469
486
  const workResultLabelValue = () => ({
470
- label: z.coerce.string().catch(""),
487
+ label: coercedString(),
471
488
  value: z.union([z.string(), z.number()]).catch(""),
472
489
  });
473
490
 
@@ -483,7 +500,7 @@ export type WorkResultMetadata = z.infer<typeof WorkResultMetadataSchema>;
483
500
 
484
501
  export const WorkResultItemSchema = z.object({
485
502
  id: tolerantString(),
486
- title: z.coerce.string().catch(""),
503
+ title: coercedString(),
487
504
  description: tolerantString(),
488
505
  status: tolerantString(),
489
506
  tone: WorkResultToneSchema.optional().catch(undefined),
@@ -506,7 +523,7 @@ export type WorkResultDiff = z.infer<typeof WorkResultDiffSchema>;
506
523
 
507
524
  export const WorkResultSectionSchema = z.object({
508
525
  id: tolerantString(),
509
- title: z.coerce.string().catch(""),
526
+ title: coercedString(),
510
527
  description: tolerantString(),
511
528
  type: WorkResultSectionTypeSchema.optional().catch(undefined),
512
529
  items: recordArray(WorkResultItemSchema).optional().catch(undefined),
@@ -523,6 +540,96 @@ export const WorkResultSurfaceDataSchema = z.object({
523
540
  });
524
541
  export type WorkResultSurfaceData = z.infer<typeof WorkResultSurfaceDataSchema>;
525
542
 
543
+ /**
544
+ * A watch retrospective: what a teach session recorded, and the few things the
545
+ * recording could not settle.
546
+ *
547
+ * Carried as the `templateData` of a `card` surface under the `watch_retro`
548
+ * template, rather than as a surface type of its own. A renderer that does not
549
+ * know the template still knows `card`, so it draws the surface's `title`,
550
+ * `subtitle` and `body` instead of an unsupported-surface notice: the retro is
551
+ * the whole of what a finished session gives the user, so it has to survive a
552
+ * client older than the template.
553
+ *
554
+ * Rendered as a paged card, one thing per page: the record first, then a page
555
+ * per question. That shape is the payload's only real constraint: a question
556
+ * list long enough to scroll is a questionnaire, so `questions` is capped at
557
+ * three and the cap is enforced here rather than asked for in the prompt.
558
+ *
559
+ * Tolerant like every other surface schema. A malformed question is dropped
560
+ * and the rest of the card still renders, because the record is the half the
561
+ * user is owed and it does not depend on the questions parsing.
562
+ */
563
+ export const WatchRetroQuestionKindSchema = z.enum(["fill", "pick", "gate"]);
564
+ export type WatchRetroQuestionKind = z.infer<
565
+ typeof WatchRetroQuestionKindSchema
566
+ >;
567
+
568
+ /**
569
+ * One answer to a `pick` or `gate` question.
570
+ *
571
+ * **The first option is the default**, and that is the whole of the
572
+ * preselection contract. There is no `selected` flag to disagree with the
573
+ * ordering. On a `pick` it is the reading the recording supports; on a `gate`
574
+ * it is the cautious answer, which is the one case where the default is
575
+ * deliberately not the model's guess.
576
+ */
577
+ export const WatchRetroOptionSchema = z.object({
578
+ id: coercedString(),
579
+ label: coercedString(),
580
+ /** Short qualifier under the label, e.g. marking the model's own reading. */
581
+ note: tolerantString(),
582
+ });
583
+ export type WatchRetroOption = z.infer<typeof WatchRetroOptionSchema>;
584
+
585
+ export const WatchRetroQuestionSchema = z.object({
586
+ id: coercedString(),
587
+ kind: WatchRetroQuestionKindSchema.catch("pick"),
588
+ prompt: coercedString(),
589
+ /** Eyebrow above the prompt, naming why this one is being asked. */
590
+ eyebrow: tolerantString(),
591
+ /** `fill` only: the pre-filled value, so skipping keeps a working answer. */
592
+ suggestion: tolerantString(),
593
+ /** `pick` and `gate`: the alternatives. First is the default. */
594
+ options: recordArray(WatchRetroOptionSchema).optional().catch(undefined),
595
+ });
596
+ export type WatchRetroQuestion = z.infer<typeof WatchRetroQuestionSchema>;
597
+
598
+ /** How many questions one card may carry. See the schema note. */
599
+ export const WATCH_RETRO_MAX_QUESTIONS = 3;
600
+
601
+ export const WatchRetroSurfaceDataSchema = z.object({
602
+ /** The task, named in one line. The card's title. */
603
+ task: coercedString(),
604
+ /** What the task is for. The one sentence that makes the steps mean something. */
605
+ purpose: tolerantString(),
606
+ /**
607
+ * The steps, in order, as imperative fragments rather than sentences:
608
+ * "Open the Sentry issue", not "You opened the Sentry issue from the alert
609
+ * email". It is about to become a procedure, so it reads as one already.
610
+ */
611
+ steps: z.preprocess(
612
+ (value) =>
613
+ Array.isArray(value)
614
+ ? value.filter((entry) => typeof entry === "string" && entry.trim())
615
+ : [],
616
+ z.array(z.string()),
617
+ ),
618
+ /** Session facts for the eyebrow, e.g. "Watched 4 min · 11 screens". */
619
+ eyebrow: tolerantString(),
620
+ /** Set when the recording was bounded, naming which end is missing. */
621
+ coverage: tolerantString(),
622
+ questions: z
623
+ .preprocess(
624
+ (value) =>
625
+ Array.isArray(value) ? value.slice(0, WATCH_RETRO_MAX_QUESTIONS) : [],
626
+ recordArray(WatchRetroQuestionSchema),
627
+ )
628
+ .optional()
629
+ .catch(undefined),
630
+ });
631
+ export type WatchRetroSurfaceData = z.infer<typeof WatchRetroSurfaceDataSchema>;
632
+
526
633
  /**
527
634
  * Inline visual: a self-contained html fragment rendered in a sandboxed
528
635
  * frame. Model-invokable via `ui_show`, which validates the fragment (no