@vellumai/assistant 0.8.12 → 0.9.0-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 (442) hide show
  1. package/AGENTS.md +0 -14
  2. package/ARCHITECTURE.md +45 -45
  3. package/README.md +1 -1
  4. package/bun.lock +200 -154
  5. package/docs/architecture/integrations.md +3 -3
  6. package/docs/architecture/memory.md +2 -2
  7. package/docs/architecture/security.md +10 -10
  8. package/docs/runbook-trusted-contacts.md +12 -12
  9. package/docs/skills.md +6 -6
  10. package/docs/workflows-testing.md +221 -0
  11. package/docs/workflows.md +510 -0
  12. package/examples/plugins/echo/README.md +5 -5
  13. package/knip.json +2 -0
  14. package/node_modules/@vellumai/gateway-client/src/inbound-contract.ts +105 -0
  15. package/node_modules/@vellumai/gateway-client/src/index.ts +12 -0
  16. package/openapi.yaml +7197 -5708
  17. package/package.json +8 -4
  18. package/scripts/generate-openapi.ts +66 -114
  19. package/src/__tests__/access-request-seed-content-blocks.test.ts +213 -0
  20. package/src/__tests__/adaptive-thinking-repair.test.ts +32 -3
  21. package/src/__tests__/agent-loop-output-hooks.test.ts +183 -0
  22. package/src/__tests__/agent-loop-regrowth-guard.test.ts +506 -0
  23. package/src/__tests__/agent-wake-disk-pressure-callsite.test.ts +2 -0
  24. package/src/__tests__/agent-wake-override-profile.test.ts +77 -0
  25. package/src/__tests__/app-compiler.test.ts +7 -1
  26. package/src/__tests__/app-dir-path-guard.test.ts +27 -3
  27. package/src/__tests__/app-executors.test.ts +43 -0
  28. package/src/__tests__/approval-cascade.test.ts +0 -5
  29. package/src/__tests__/approval-routes-http.test.ts +91 -0
  30. package/src/__tests__/assistant-stream-state.test.ts +107 -0
  31. package/src/__tests__/browser-fill-credential.test.ts +3 -3
  32. package/src/__tests__/bundled-skill-retrieval-guard.test.ts +1 -1
  33. package/src/__tests__/compaction-events.test.ts +63 -7
  34. package/src/__tests__/compaction-trail-store.test.ts +74 -1
  35. package/src/__tests__/compaction.benchmark.test.ts +63 -41
  36. package/src/__tests__/compactor-low-watermark-cut.test.ts +349 -0
  37. package/src/__tests__/context-window-manager-compact-retry.test.ts +64 -0
  38. package/src/__tests__/conversation-abort-tool-results.test.ts +0 -5
  39. package/src/__tests__/conversation-confirmation-signals.test.ts +0 -5
  40. package/src/__tests__/conversation-history-web-search.test.ts +7 -0
  41. package/src/__tests__/conversation-process-callsite.test.ts +0 -5
  42. package/src/__tests__/conversation-provider-retry-repair.test.ts +0 -5
  43. package/src/__tests__/conversation-queue.test.ts +0 -5
  44. package/src/__tests__/conversation-slash-queue.test.ts +0 -5
  45. package/src/__tests__/conversation-slash-unknown.test.ts +0 -5
  46. package/src/__tests__/conversation-speed-override.test.ts +0 -5
  47. package/src/__tests__/conversation-surfaces-data-persist.test.ts +97 -0
  48. package/src/__tests__/conversation-surfaces-task-progress.test.ts +67 -0
  49. package/src/__tests__/conversation-usage.test.ts +2 -0
  50. package/src/__tests__/conversation-workspace-injection.test.ts +0 -5
  51. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +0 -5
  52. package/src/__tests__/credential-broker-browser-fill.test.ts +2 -2
  53. package/src/__tests__/credential-broker-server-use.test.ts +2 -2
  54. package/src/__tests__/credential-broker.test.ts +1 -1
  55. package/src/__tests__/credential-prompt-route.test.ts +417 -0
  56. package/src/__tests__/credential-security-invariants.test.ts +1 -0
  57. package/src/__tests__/credential-vault.test.ts +37 -0
  58. package/src/__tests__/db-schedule-syntax-migration.test.ts +24 -0
  59. package/src/__tests__/dynamic-page-surface.test.ts +219 -0
  60. package/src/__tests__/empty-state-greeting-cache.test.ts +94 -0
  61. package/src/__tests__/gateway-flag-listener.test.ts +24 -7
  62. package/src/__tests__/guardian-action-sweep.test.ts +56 -219
  63. package/src/__tests__/guardian-routing-invariants.test.ts +138 -0
  64. package/src/__tests__/helpers/channel-test-adapter.ts +0 -2
  65. package/src/__tests__/list-messages-hidden-metadata.test.ts +99 -0
  66. package/src/__tests__/llm-request-log-source-clickhouse.test.ts +87 -1
  67. package/src/__tests__/llm-resolver.test.ts +115 -0
  68. package/src/__tests__/managed-profile-guard.test.ts +6 -5
  69. package/src/__tests__/max-tokens-continue-hook.test.ts +184 -0
  70. package/src/__tests__/media-generate-image.test.ts +20 -9
  71. package/src/__tests__/mock-gateway-ipc.ts +23 -0
  72. package/src/__tests__/model-intents.test.ts +1 -1
  73. package/src/__tests__/normalize-onboarding.test.ts +26 -0
  74. package/src/__tests__/notification-decision-strategy.test.ts +4 -2
  75. package/src/__tests__/notification-telegram-adapter.test.ts +21 -3
  76. package/src/__tests__/pending-interactions-resolved-event.test.ts +62 -0
  77. package/src/__tests__/post-turn-tool-result-truncation.test.ts +72 -18
  78. package/src/__tests__/require-fresh-approval.test.ts +425 -1
  79. package/src/__tests__/resolve-app-id.test.ts +56 -0
  80. package/src/__tests__/runtime-events-sse-parity.test.ts +2 -0
  81. package/src/__tests__/schedule-routes-workflow-validation.test.ts +408 -0
  82. package/src/__tests__/schedule-routes.test.ts +257 -4
  83. package/src/__tests__/schedule-store.test.ts +60 -0
  84. package/src/__tests__/schedule-tools.test.ts +247 -2
  85. package/src/__tests__/skill-execute-input.test.ts +85 -0
  86. package/src/__tests__/skill-secret-handling-guard.test.ts +21 -20
  87. package/src/__tests__/skills.test.ts +3 -3
  88. package/src/__tests__/slack-app-setup-skill-regression.test.ts +1 -1
  89. package/src/__tests__/subagent-tool-filtering.test.ts +50 -0
  90. package/src/__tests__/subagent-tool-gate-mode.test.ts +547 -0
  91. package/src/__tests__/system-prompt.test.ts +1 -1
  92. package/src/__tests__/task-progress-nudge-hook.test.ts +372 -0
  93. package/src/__tests__/task-scheduler.test.ts +299 -0
  94. package/src/__tests__/tool-approval-seed-content-blocks.test.ts +209 -0
  95. package/src/__tests__/tool-result-spool.test.ts +3 -1
  96. package/src/__tests__/workspace-migration-102-preserve-heartbeat-enabled-for-existing-workspaces.test.ts +181 -0
  97. package/src/__tests__/workspace-migration-103-upgrade-quality-profile-to-opus-4-8.test.ts +174 -0
  98. package/src/agent/compaction-circuit.ts +11 -0
  99. package/src/agent/loop.ts +181 -12
  100. package/src/api/constants/call-sites.ts +12 -0
  101. package/src/api/events/assistant-thinking-delta.ts +10 -0
  102. package/src/api/events/usage-update.ts +7 -0
  103. package/src/api/index.ts +4 -1
  104. package/src/api/responses/memory-v3-selection-log.ts +18 -11
  105. package/src/approvals/approval-primitive.ts +2 -2
  106. package/src/background-wake/background-wake-routes.test.ts +5 -2
  107. package/src/bundler/compiler-tools.ts +1 -1
  108. package/src/bundler/package-resolver.ts +0 -1
  109. package/src/calls/call-domain.ts +1 -1
  110. package/src/calls/guardian-action-sweep.ts +16 -93
  111. package/src/cli/AGENTS.md +4 -0
  112. package/src/cli/commands/__tests__/schedules.test.ts +430 -1
  113. package/src/cli/commands/credentials.ts +28 -24
  114. package/src/cli/commands/image-generation.ts +23 -9
  115. package/src/cli/commands/notifications.ts +1 -1
  116. package/src/cli/commands/plugins.ts +89 -46
  117. package/src/cli/commands/schedules.ts +384 -11
  118. package/src/cli/lib/__tests__/inspect-plugin.test.ts +69 -5
  119. package/src/cli/lib/__tests__/install-from-github.test.ts +15 -0
  120. package/src/cli/lib/__tests__/upgrade-plugin.test.ts +81 -4
  121. package/src/cli/lib/inspect-plugin.ts +62 -1
  122. package/src/cli/lib/install-from-github.ts +52 -5
  123. package/src/cli/lib/upgrade-plugin.ts +18 -0
  124. package/src/config/__tests__/workflows-schema.test.ts +60 -0
  125. package/src/config/bundled-skills/acp/SKILL.md +2 -2
  126. package/src/config/bundled-skills/app-builder/SKILL.md +1 -1
  127. package/src/config/bundled-skills/app-builder/tools/app-create.ts +6 -1
  128. package/src/config/bundled-skills/app-builder/tools/app-generate-icon.ts +7 -1
  129. package/src/config/bundled-skills/app-builder/tools/app-refresh.ts +7 -1
  130. package/src/config/bundled-skills/app-builder/tools/app-update.ts +10 -1
  131. package/src/config/bundled-skills/image-studio/SKILL.md +66 -19
  132. package/src/config/bundled-skills/image-studio/TOOLS.json +1 -6
  133. package/src/config/bundled-skills/image-studio/tools/media-generate-image.ts +22 -3
  134. package/src/config/bundled-skills/personal-page/SKILL.md +57 -0
  135. package/src/config/bundled-skills/personal-page/TOOLS.json +27 -0
  136. package/src/config/bundled-skills/personal-page/tools/app-refresh.ts +17 -0
  137. package/src/config/bundled-skills/schedule/SKILL.md +7 -2
  138. package/src/config/bundled-skills/schedule/TOOLS.json +48 -4
  139. package/src/config/bundled-skills/workflows/SKILL.md +214 -0
  140. package/src/config/bundled-skills/workflows/TOOLS.json +84 -0
  141. package/src/config/bundled-skills/workflows/tools/manage-workflows.ts +12 -0
  142. package/src/config/bundled-skills/workflows/tools/run-workflow.ts +12 -0
  143. package/src/config/bundled-tool-registry.ts +14 -2
  144. package/src/config/call-site-defaults.ts +5 -0
  145. package/src/config/feature-flag-registry.json +12 -4
  146. package/src/config/llm-context-resolution.ts +8 -0
  147. package/src/config/llm-resolver.ts +30 -0
  148. package/src/config/preloaded-apps/personal-page/src/components/About.tsx +22 -0
  149. package/src/config/preloaded-apps/personal-page/src/components/App.tsx +16 -0
  150. package/src/config/preloaded-apps/personal-page/src/components/Features.tsx +77 -0
  151. package/src/config/preloaded-apps/personal-page/src/components/Hero.tsx +57 -0
  152. package/src/config/preloaded-apps/personal-page/src/components/Pending.tsx +28 -0
  153. package/src/config/preloaded-apps/personal-page/src/components/animations.tsx +234 -0
  154. package/src/config/preloaded-apps/personal-page/src/components/icons.tsx +48 -0
  155. package/src/config/preloaded-apps/personal-page/src/components/media.ts +16 -0
  156. package/src/config/preloaded-apps/personal-page/src/index.html +20 -0
  157. package/src/config/preloaded-apps/personal-page/src/main.tsx +7 -0
  158. package/src/config/preloaded-apps/personal-page/src/profile-data.ts +82 -0
  159. package/src/config/preloaded-apps/personal-page/src/styles.css +759 -0
  160. package/src/config/schema.ts +2 -0
  161. package/src/config/schemas/call-site-catalog.ts +7 -0
  162. package/src/config/schemas/heartbeat.ts +4 -1
  163. package/src/config/schemas/llm.ts +33 -26
  164. package/src/config/schemas/memory-retrospective.ts +19 -0
  165. package/src/config/schemas/platform.ts +8 -0
  166. package/src/config/schemas/services.ts +5 -2
  167. package/src/config/schemas/workflows.ts +42 -0
  168. package/src/config/skills.ts +3 -3
  169. package/src/context/compactor.ts +273 -39
  170. package/src/context/post-turn-tool-result-truncation.ts +23 -6
  171. package/src/context/tool-result-spool.ts +12 -17
  172. package/src/credential-execution/executable-discovery.ts +1 -1
  173. package/src/credential-execution/process-manager.ts +37 -3
  174. package/src/credential-execution/prompted-credential.ts +205 -0
  175. package/src/daemon/conversation-agent-loop-handlers.ts +14 -0
  176. package/src/daemon/conversation-process.ts +11 -2
  177. package/src/daemon/conversation-surfaces.ts +167 -3
  178. package/src/daemon/conversation-tool-setup.ts +103 -26
  179. package/src/daemon/conversation-usage.ts +2 -0
  180. package/src/daemon/conversation.ts +115 -11
  181. package/src/daemon/handlers/shared.ts +26 -14
  182. package/src/daemon/host-cu-proxy.ts +15 -12
  183. package/src/daemon/host-file-proxy.ts +15 -12
  184. package/src/daemon/host-transfer-proxy.ts +30 -24
  185. package/src/daemon/lifecycle.ts +40 -3
  186. package/src/daemon/message-protocol.ts +3 -0
  187. package/src/daemon/message-types/messages.ts +2 -10
  188. package/src/daemon/message-types/workflows.ts +49 -0
  189. package/src/daemon/parse-actual-tokens-from-error.test.ts +62 -1
  190. package/src/daemon/parse-actual-tokens-from-error.ts +43 -4
  191. package/src/daemon/process-message.ts +6 -0
  192. package/src/daemon/tool-setup-types.ts +57 -0
  193. package/src/daemon/wake-conversation-ops.ts +18 -0
  194. package/src/heartbeat/heartbeat-run-store.ts +8 -2
  195. package/src/home/feed-types.ts +1 -1
  196. package/src/ipc/gateway-flag-listener.ts +28 -6
  197. package/src/mcp/mcp-auth-state.ts +8 -20
  198. package/src/media/__tests__/image-models.test.ts +57 -0
  199. package/src/media/image-models.ts +66 -0
  200. package/src/memory/__tests__/auto-analysis-enqueue.test.ts +38 -0
  201. package/src/memory/__tests__/find-most-recent-retrospective-for.test.ts +12 -2
  202. package/src/memory/__tests__/memory-retrospective-job.test.ts +911 -34
  203. package/src/memory/__tests__/memory-retrospective-startup-cleanup.test.ts +227 -5
  204. package/src/memory/__tests__/memory-retrospective-state.test.ts +195 -0
  205. package/src/memory/__tests__/preloaded-apps.test.ts +85 -0
  206. package/src/memory/auto-analysis-enqueue.ts +14 -1
  207. package/src/memory/compaction-log-store-clickhouse.ts +6 -4
  208. package/src/memory/conversation-crud.ts +9 -2
  209. package/src/memory/conversation-disk-view.ts +1 -1
  210. package/src/memory/conversation-queries.ts +22 -7
  211. package/src/memory/db-init.ts +20 -0
  212. package/src/memory/db-maintenance.ts +16 -0
  213. package/src/memory/embedding-runtime-manager.ts +1 -1
  214. package/src/memory/llm-request-log-source-clickhouse.ts +112 -14
  215. package/src/memory/llm-request-log-source-local.ts +19 -1
  216. package/src/memory/llm-request-log-source.ts +35 -6
  217. package/src/memory/llm-request-log-store.ts +90 -2
  218. package/src/memory/memory-retrospective-constants.ts +9 -0
  219. package/src/memory/memory-retrospective-enqueue.ts +3 -6
  220. package/src/memory/memory-retrospective-fork-boundary.ts +94 -0
  221. package/src/memory/memory-retrospective-job.ts +500 -208
  222. package/src/memory/memory-retrospective-startup-cleanup.ts +97 -19
  223. package/src/memory/memory-retrospective-state.ts +85 -2
  224. package/src/memory/migrations/281-memory-retrospective-remembered-log.ts +40 -0
  225. package/src/memory/migrations/282-schedule-inference-profile.test.ts +77 -0
  226. package/src/memory/migrations/282-schedule-inference-profile.ts +26 -0
  227. package/src/memory/migrations/283-memory-v3-selections-message-id-and-sections.test.ts +102 -0
  228. package/src/memory/migrations/283-memory-v3-selections-message-id-and-sections.ts +53 -0
  229. package/src/memory/migrations/284-workflow-runs.ts +51 -0
  230. package/src/memory/migrations/285-schedule-workflow-mode.ts +26 -0
  231. package/src/memory/migrations/286-workflow-run-trust.ts +27 -0
  232. package/src/memory/migrations/287-conversation-origin-channel-index.ts +15 -0
  233. package/src/memory/migrations/288-backfill-origin-channel-from-bindings.ts +43 -0
  234. package/src/memory/migrations/289-contact-channels-unique-ext-user.ts +115 -0
  235. package/src/memory/migrations/290-schedule-capabilities.test.ts +77 -0
  236. package/src/memory/migrations/290-schedule-capabilities.ts +25 -0
  237. package/src/memory/migrations/__tests__/281-memory-retrospective-remembered-log.test.ts +96 -0
  238. package/src/memory/migrations/__tests__/289-contact-channels-unique-ext-user.test.ts +571 -0
  239. package/src/memory/migrations/index.ts +10 -0
  240. package/src/memory/preloaded-apps.ts +116 -0
  241. package/src/memory/schema/infrastructure.ts +4 -0
  242. package/src/memory/schema/memory-core.ts +4 -0
  243. package/src/memory/v2/__tests__/concept-page-frontmatter-schema.test.ts +45 -0
  244. package/src/memory/v2/__tests__/frontmatter-sweep.test.ts +11 -7
  245. package/src/memory/v2/__tests__/page-store.test.ts +13 -2
  246. package/src/memory/v2/__tests__/qdrant.test.ts +24 -0
  247. package/src/memory/v2/frontmatter-sweep.ts +7 -6
  248. package/src/memory/v2/page-store.ts +4 -3
  249. package/src/memory/v2/qdrant.ts +42 -3
  250. package/src/memory/v2/types.ts +16 -10
  251. package/src/messaging/draft-store.ts +1 -1
  252. package/src/notifications/access-request-copy.ts +200 -113
  253. package/src/notifications/adapters/slack.ts +250 -111
  254. package/src/notifications/adapters/telegram.ts +7 -44
  255. package/src/notifications/approval-card-builder.ts +93 -0
  256. package/src/notifications/broadcaster.ts +74 -0
  257. package/src/notifications/conversation-pairing.ts +8 -6
  258. package/src/notifications/copy-composer.ts +32 -26
  259. package/src/notifications/decision-engine.ts +59 -7
  260. package/src/notifications/guardian-question-mode.ts +145 -155
  261. package/src/notifications/home-feed-side-effect.ts +28 -11
  262. package/src/notifications/notification-utils.ts +66 -0
  263. package/src/notifications/signal.ts +6 -0
  264. package/src/notifications/tool-approval-copy.ts +142 -0
  265. package/src/notifications/types.ts +19 -0
  266. package/src/permissions/threshold.ts +11 -0
  267. package/src/plugin-api/types.ts +16 -4
  268. package/src/plugins/defaults/compaction/window-manager.ts +44 -0
  269. package/src/plugins/defaults/index.ts +46 -0
  270. package/src/plugins/defaults/max-tokens-continue/continue-state-store.ts +53 -0
  271. package/src/plugins/defaults/max-tokens-continue/hooks/post-model-call.ts +80 -0
  272. package/src/plugins/defaults/max-tokens-continue/hooks/stop.ts +20 -0
  273. package/src/plugins/defaults/max-tokens-continue/package.json +14 -0
  274. package/src/plugins/defaults/memory-retrieval/hooks/__tests__/user-prompt-submit.test.ts +37 -0
  275. package/src/plugins/defaults/memory-retrieval/hooks/user-prompt-submit.ts +29 -1
  276. package/src/plugins/defaults/memory-v3-shadow/__tests__/carry-integration.test.ts +8 -3
  277. package/src/plugins/defaults/memory-v3-shadow/__tests__/injection.test.ts +4 -2
  278. package/src/plugins/defaults/memory-v3-shadow/__tests__/pool-select.test.ts +28 -18
  279. package/src/plugins/defaults/memory-v3-shadow/__tests__/section-dense-store.test.ts +67 -0
  280. package/src/plugins/defaults/memory-v3-shadow/__tests__/selection-log-store.test.ts +122 -22
  281. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-integration.test.ts +2 -0
  282. package/src/plugins/defaults/memory-v3-shadow/__tests__/shadow-plugin.test.ts +63 -1
  283. package/src/plugins/defaults/memory-v3-shadow/injector.ts +61 -18
  284. package/src/plugins/defaults/memory-v3-shadow/orchestrate.ts +1 -1
  285. package/src/plugins/defaults/memory-v3-shadow/pool-select.ts +39 -10
  286. package/src/plugins/defaults/memory-v3-shadow/section-dense-store.ts +34 -1
  287. package/src/plugins/defaults/memory-v3-shadow/selection-log-store.ts +112 -47
  288. package/src/plugins/defaults/memory-v3-shadow/shadow-plugin.ts +78 -15
  289. package/src/plugins/defaults/task-progress-nudge/hooks/post-tool-use.ts +206 -0
  290. package/src/plugins/defaults/task-progress-nudge/package.json +15 -0
  291. package/src/prompts/__tests__/system-prompt.test.ts +100 -1
  292. package/src/prompts/__tests__/task-progress-hint-section.test.ts +5 -7
  293. package/src/prompts/normalize-onboarding.ts +2 -0
  294. package/src/prompts/persona-resolver.ts +3 -0
  295. package/src/prompts/system-prompt.ts +51 -2
  296. package/src/prompts/templates/BOOTSTRAP-ACTIVATION-RAIL.md +3 -1
  297. package/src/prompts/templates/system-sections.ts +8 -3
  298. package/src/providers/call-site-routing.ts +6 -3
  299. package/src/providers/fireworks/client.ts +3 -0
  300. package/src/providers/inference/auth.ts +52 -46
  301. package/src/providers/model-intents.ts +2 -2
  302. package/src/providers/openai/__tests__/coerce-object-args.test.ts +105 -0
  303. package/src/providers/openai/chat-completions-provider.ts +47 -9
  304. package/src/providers/openai/coerce-object-args.ts +104 -0
  305. package/src/providers/retry.ts +8 -5
  306. package/src/providers/types.ts +10 -0
  307. package/src/runtime/__tests__/agent-wake.test.ts +629 -7
  308. package/src/runtime/access-request-helper.ts +16 -9
  309. package/src/runtime/agent-wake.ts +302 -51
  310. package/src/runtime/assistant-stream-state.ts +141 -8
  311. package/src/runtime/background-job-runner.ts +9 -0
  312. package/src/runtime/channel-approval-types.ts +1 -0
  313. package/src/runtime/channel-invite-transports/telegram.ts +6 -5
  314. package/src/runtime/channel-invite-transports/voice.ts +2 -2
  315. package/src/runtime/channel-invite-types.ts +4 -2
  316. package/src/runtime/channel-retry-sweep.ts +19 -41
  317. package/src/runtime/finalize-event-delivery.ts +72 -0
  318. package/src/runtime/guardian-action-message-composer.ts +0 -54
  319. package/src/runtime/http-server.ts +6 -14
  320. package/src/runtime/http-types.ts +0 -1
  321. package/src/runtime/message-composer-types.ts +0 -9
  322. package/src/runtime/middleware/__tests__/rate-limiter.test.ts +63 -0
  323. package/src/runtime/middleware/auth.ts +27 -3
  324. package/src/runtime/middleware/rate-limiter.ts +28 -1
  325. package/src/runtime/migrations/vbundle-builder.ts +6 -5
  326. package/src/runtime/pending-interactions.ts +20 -1
  327. package/src/runtime/routes/__tests__/conversation-compaction-routes.test.ts +232 -173
  328. package/src/runtime/routes/__tests__/plugins-routes.test.ts +18 -0
  329. package/src/runtime/routes/__tests__/retrospective-routes.test.ts +436 -0
  330. package/src/runtime/routes/__tests__/surface-action-routes.test.ts +11 -0
  331. package/src/runtime/routes/approval-routes.ts +35 -8
  332. package/src/runtime/routes/approval-strategies/guardian-callback-strategy.ts +2 -1
  333. package/src/runtime/routes/btw-routes.ts +37 -1
  334. package/src/runtime/routes/channel-delivery-routes.ts +11 -7
  335. package/src/runtime/routes/channel-route-definitions.ts +3 -0
  336. package/src/runtime/routes/channel-route-shared.ts +3 -1
  337. package/src/runtime/routes/consolidation-routes.ts +17 -13
  338. package/src/runtime/routes/conversation-compaction-routes.ts +159 -119
  339. package/src/runtime/routes/conversation-list-routes.ts +41 -4
  340. package/src/runtime/routes/conversation-query-routes.ts +196 -11
  341. package/src/runtime/routes/conversation-routes.ts +15 -1
  342. package/src/runtime/routes/credential-prompt-routes.ts +39 -17
  343. package/src/runtime/routes/empty-state-greeting-cache.ts +65 -0
  344. package/src/runtime/routes/heartbeat-routes.ts +18 -13
  345. package/src/runtime/routes/image-generation-routes.ts +20 -2
  346. package/src/runtime/routes/inbound-message-handler.ts +13 -12
  347. package/src/runtime/routes/inbound-stages/acl-enforcement.ts +32 -31
  348. package/src/runtime/routes/inbound-stages/background-dispatch.test.ts +7 -5
  349. package/src/runtime/routes/inbound-stages/background-dispatch.ts +7 -21
  350. package/src/runtime/routes/inbound-stages/escalation-intercept.ts +5 -5
  351. package/src/runtime/routes/inbound-stages/guardian-activation-intercept.ts +6 -15
  352. package/src/runtime/routes/inbound-stages/secret-ingress-check.ts +1 -1
  353. package/src/runtime/routes/index.ts +6 -0
  354. package/src/runtime/routes/log-export/AGENTS.md +1 -1
  355. package/src/runtime/routes/log-export/workspace-allowlist.ts +1 -1
  356. package/src/runtime/routes/migration-routes.ts +5 -9
  357. package/src/runtime/routes/plugins-routes.ts +26 -0
  358. package/src/runtime/routes/ps-routes.ts +10 -8
  359. package/src/runtime/routes/retrospective-routes.ts +235 -0
  360. package/src/runtime/routes/runs-pagination.ts +75 -0
  361. package/src/runtime/routes/schedule-routes.ts +367 -59
  362. package/src/runtime/routes/sounds-config-routes.ts +239 -0
  363. package/src/runtime/routes/surface-action-routes.ts +84 -4
  364. package/src/runtime/routes/workflow-routes.test.ts +372 -0
  365. package/src/runtime/routes/workflow-routes.ts +363 -0
  366. package/src/runtime/routes/workspace-routes.test.ts +61 -1
  367. package/src/runtime/routes/workspace-routes.ts +26 -1
  368. package/src/runtime/services/__tests__/analyze-conversation.test.ts +38 -0
  369. package/src/runtime/services/analyze-conversation.ts +26 -13
  370. package/src/schedule/inference-profile.ts +28 -0
  371. package/src/schedule/schedule-store.ts +152 -4
  372. package/src/schedule/scheduler-types.ts +6 -0
  373. package/src/schedule/scheduler.ts +96 -0
  374. package/src/security/secret-allowlist.ts +1 -1
  375. package/src/skills/path-classifier.ts +1 -1
  376. package/src/tools/apps/executors.ts +23 -0
  377. package/src/tools/apps/resolve-app-id.ts +42 -0
  378. package/src/tools/browser/browser-execution.ts +9 -11
  379. package/src/tools/credentials/broker.ts +4 -4
  380. package/src/tools/credentials/vault.ts +26 -137
  381. package/src/tools/executor.ts +69 -0
  382. package/src/tools/flag-gated-tools.test.ts +76 -0
  383. package/src/tools/permission-checker.ts +8 -1
  384. package/src/tools/registry.ts +51 -0
  385. package/src/tools/schedule/create.ts +77 -1
  386. package/src/tools/schedule/list.ts +1 -0
  387. package/src/tools/schedule/update.ts +73 -1
  388. package/src/tools/skills/execute.ts +56 -0
  389. package/src/tools/terminal/shell.ts +1 -1
  390. package/src/tools/ui-surface/definitions.ts +91 -2
  391. package/src/tools/workflows/manage-workflows.ts +183 -0
  392. package/src/tools/workflows/run-workflow.test.ts +442 -0
  393. package/src/tools/workflows/run-workflow.ts +88 -0
  394. package/src/types/onboarding-context.ts +2 -0
  395. package/src/usage/attribution.ts +24 -0
  396. package/src/util/canonicalize-identity.ts +12 -3
  397. package/src/util/platform.ts +17 -17
  398. package/src/watcher/__tests__/engine.test.ts +24 -0
  399. package/src/watcher/__tests__/telemetry.test.ts +135 -0
  400. package/src/watcher/engine.ts +7 -0
  401. package/src/watcher/telemetry.ts +74 -0
  402. package/src/workflows/capabilities.test.ts +365 -0
  403. package/src/workflows/capabilities.ts +359 -0
  404. package/src/workflows/deterministic-stringify.ts +27 -0
  405. package/src/workflows/engine-integration.test.ts +656 -0
  406. package/src/workflows/engine.test.ts +1144 -0
  407. package/src/workflows/engine.ts +1078 -0
  408. package/src/workflows/fanout-load.test.ts +168 -0
  409. package/src/workflows/journal-store.test.ts +369 -0
  410. package/src/workflows/journal-store.ts +470 -0
  411. package/src/workflows/leaf-runner.test.ts +704 -0
  412. package/src/workflows/leaf-runner.ts +589 -0
  413. package/src/workflows/library.test.ts +134 -0
  414. package/src/workflows/library.ts +124 -0
  415. package/src/workflows/run-manager.test.ts +711 -0
  416. package/src/workflows/run-manager.ts +593 -0
  417. package/src/workflows/sandbox-escape.test.ts +339 -0
  418. package/src/workflows/sandbox.test.ts +251 -0
  419. package/src/workflows/sandbox.ts +447 -0
  420. package/src/workspace/adaptive-thinking-repair.ts +33 -11
  421. package/src/workspace/migrations/021-move-signals-to-workspace.ts +1 -1
  422. package/src/workspace/migrations/022-move-hooks-to-workspace.ts +1 -1
  423. package/src/workspace/migrations/026-backfill-install-meta.ts +1 -1
  424. package/src/workspace/migrations/030-seed-pkb-autoinject.ts +2 -1
  425. package/src/workspace/migrations/031-drop-user-md.ts +1 -4
  426. package/src/workspace/migrations/048-remove-workspace-hooks.ts +1 -1
  427. package/src/workspace/migrations/056-release-notes-inference-profile-reordering.ts +5 -2
  428. package/src/workspace/migrations/061-move-backup-key-to-workspace.ts +1 -1
  429. package/src/workspace/migrations/082-backfill-managed-profile-labels.ts +8 -2
  430. package/src/workspace/migrations/097-enable-adaptive-thinking-managed-profiles.ts +41 -14
  431. package/src/workspace/migrations/102-preserve-heartbeat-enabled-for-existing-workspaces.ts +69 -0
  432. package/src/workspace/migrations/103-upgrade-quality-profile-to-opus-4-8.ts +83 -0
  433. package/src/workspace/migrations/104-recheck-adaptive-thinking-model-implied-anthropic.ts +133 -0
  434. package/src/workspace/migrations/registry.ts +6 -0
  435. package/src/workspace/migrations/runner.ts +1 -1
  436. package/tsconfig.json +1 -1
  437. package/src/__tests__/guardian-action-copy-generator.test.ts +0 -200
  438. package/src/__tests__/guardian-action-grant-mint-consume.test.ts +0 -579
  439. package/src/__tests__/guardian-action-store.test.ts +0 -106
  440. package/src/daemon/guardian-action-generators.ts +0 -71
  441. package/src/memory/guardian-action-store.ts +0 -484
  442. package/src/runtime/guardian-action-grant-minter.ts +0 -150
@@ -4,13 +4,29 @@
4
4
  * Explicit request kinds provide a stable contract between producers and
5
5
  * notification rendering logic, avoiding implicit inference from incidental
6
6
  * fields like `toolName`.
7
+ *
8
+ * Payload shapes are defined as Zod schemas — single source of truth for
9
+ * both runtime validation and TypeScript types.
10
+ * https://zod.dev/?id=basic-usage
7
11
  */
8
12
 
9
- type GuardianQuestionRequestKind =
10
- | "pending_question"
11
- | "tool_approval"
12
- | "tool_grant_request"
13
- | "access_request";
13
+ import { z } from "zod";
14
+
15
+ import { nonEmpty } from "./notification-utils.js";
16
+
17
+ // ── Schema primitives ──────────────────────────────────────────────────
18
+
19
+ export const GuardianQuestionRequestKindSchema = z.enum([
20
+ "pending_question",
21
+ "tool_approval",
22
+ "tool_grant_request",
23
+ "access_request",
24
+ ]);
25
+
26
+ export type GuardianQuestionRequestKind = z.infer<
27
+ typeof GuardianQuestionRequestKindSchema
28
+ >;
29
+
14
30
  type GuardianQuestionInstructionMode = "approval" | "answer";
15
31
 
16
32
  interface GuardianRequestKindModeConfig {
@@ -37,16 +53,82 @@ const REQUEST_KIND_MODE_CONFIG: Record<
37
53
  },
38
54
  };
39
55
 
40
- interface GuardianQuestionPayloadBase {
41
- requestId: string;
42
- requestCode: string;
43
- questionText: string;
44
- }
56
+ // ── Zod schemas for guardian.question payloads ──────────────────────────
57
+
58
+ const GuardianQuestionPayloadBaseSchema = z.object({
59
+ requestId: z.string().min(1),
60
+ requestCode: z.string().min(1),
61
+ questionText: z.string().min(1),
62
+ /** Channel the request originated from. Set by producers but previously
63
+ * invisible to the type system (passed via index signature). */
64
+ sourceChannel: z.string().optional(),
65
+ /** Display name or identifier of the requester. */
66
+ requesterIdentifier: z.string().optional(),
67
+ /** External user ID of the requester (e.g. Slack user ID). */
68
+ requesterExternalUserId: z.string().optional(),
69
+ /** External chat ID of the requester. */
70
+ requesterChatId: z.string().nullable().optional(),
71
+ });
72
+
73
+ export const PendingQuestionPayloadSchema =
74
+ GuardianQuestionPayloadBaseSchema.extend({
75
+ requestKind: z.literal("pending_question"),
76
+ callSessionId: z.string().min(1),
77
+ activeGuardianRequestCount: z.number(),
78
+ toolName: z.string().optional(),
79
+ });
45
80
 
46
- interface GuardianQuestionPayloadBaseWithDiscriminator extends GuardianQuestionPayloadBase {
47
- requestKind: GuardianQuestionRequestKind;
48
- [key: string]: unknown;
49
- }
81
+ export const ToolApprovalPayloadSchema =
82
+ GuardianQuestionPayloadBaseSchema.extend({
83
+ requestKind: z.literal("tool_approval"),
84
+ toolName: z.string().min(1),
85
+ });
86
+
87
+ export const ToolGrantPayloadSchema = GuardianQuestionPayloadBaseSchema.extend({
88
+ requestKind: z.literal("tool_grant_request"),
89
+ toolName: z.string().min(1),
90
+ });
91
+
92
+ export const AccessRequestGuardianPayloadSchema =
93
+ GuardianQuestionPayloadBaseSchema.extend({
94
+ requestKind: z.literal("access_request"),
95
+ });
96
+
97
+ export const GuardianQuestionPayloadSchema = z.discriminatedUnion(
98
+ "requestKind",
99
+ [
100
+ PendingQuestionPayloadSchema,
101
+ ToolApprovalPayloadSchema,
102
+ ToolGrantPayloadSchema,
103
+ AccessRequestGuardianPayloadSchema,
104
+ ],
105
+ );
106
+
107
+ /**
108
+ * Lenient schema for tool-approval rendering. Requires only `requestKind`
109
+ * (for mode detection) — everything else is optional. Handles partially
110
+ * constructed payloads that don't satisfy the strict discriminated union
111
+ * (e.g. missing `callSessionId` on a `pending_question` with `toolName`).
112
+ *
113
+ * Used by `buildToolApprovalSeedContentBlocks` which must degrade
114
+ * gracefully rather than refuse to render when optional card fields
115
+ * are absent.
116
+ */
117
+ export const LenientToolApprovalPayloadSchema = z.object({
118
+ requestKind: GuardianQuestionRequestKindSchema,
119
+ requestId: z.string().nullable().optional(),
120
+ requestCode: z.string().nullable().optional(),
121
+ questionText: z.string().nullable().optional(),
122
+ toolName: z.string().nullable().optional(),
123
+ sourceChannel: z.string().nullable().optional(),
124
+ requesterIdentifier: z.string().nullable().optional(),
125
+ requesterExternalUserId: z.string().nullable().optional(),
126
+ requesterChatId: z.string().nullable().optional(),
127
+ });
128
+
129
+ export type LenientToolApprovalPayload = z.infer<
130
+ typeof LenientToolApprovalPayloadSchema
131
+ >;
50
132
 
51
133
  interface GuardianRequestModeInput {
52
134
  kind: unknown;
@@ -105,136 +187,39 @@ const MODE_TEXT_CONFIG: Record<
105
187
  },
106
188
  };
107
189
 
108
- export interface PendingQuestionGuardianPayload extends GuardianQuestionPayloadBaseWithDiscriminator {
109
- requestKind: "pending_question";
110
- callSessionId: string;
111
- activeGuardianRequestCount: number;
112
- /**
113
- * Voice tool-approval requests are persisted as pending_question with tool
114
- * metadata so they still route through pending-question resolution.
115
- */
116
- toolName?: string;
117
- }
118
-
119
- export interface ToolApprovalGuardianPayload extends GuardianQuestionPayloadBaseWithDiscriminator {
120
- requestKind: "tool_approval";
121
- toolName: string;
122
- }
123
-
124
- export interface ToolGrantGuardianPayload extends GuardianQuestionPayloadBaseWithDiscriminator {
125
- requestKind: "tool_grant_request";
126
- toolName: string;
127
- }
128
-
129
- export interface AccessRequestGuardianPayload extends GuardianQuestionPayloadBaseWithDiscriminator {
130
- requestKind: "access_request";
131
- }
132
-
133
- export type GuardianQuestionPayload =
134
- | PendingQuestionGuardianPayload
135
- | ToolApprovalGuardianPayload
136
- | ToolGrantGuardianPayload
137
- | AccessRequestGuardianPayload;
190
+ // ── Derived TypeScript types ─────────────────────────────────────────────
191
+
192
+ export type PendingQuestionGuardianPayload = z.infer<
193
+ typeof PendingQuestionPayloadSchema
194
+ >;
195
+ export type ToolApprovalGuardianPayload = z.infer<
196
+ typeof ToolApprovalPayloadSchema
197
+ >;
198
+ export type ToolGrantGuardianPayload = z.infer<typeof ToolGrantPayloadSchema>;
199
+ export type AccessRequestGuardianPayload = z.infer<
200
+ typeof AccessRequestGuardianPayloadSchema
201
+ >;
202
+ export type GuardianQuestionPayload = z.infer<
203
+ typeof GuardianQuestionPayloadSchema
204
+ >;
138
205
 
139
206
  interface GuardianQuestionModeResolution {
140
207
  mode: GuardianQuestionInstructionMode;
141
208
  requestKind: GuardianQuestionRequestKind | null;
142
209
  }
143
210
 
144
- function nonEmptyString(value: unknown): string | null {
145
- if (typeof value !== "string") return null;
146
- const trimmed = value.trim();
147
- return trimmed.length > 0 ? trimmed : null;
148
- }
149
-
150
- function parseGuardianQuestionRequestKind(
151
- payload: Record<string, unknown>,
152
- ): GuardianQuestionRequestKind | null {
153
- const raw = nonEmptyString(payload.requestKind);
154
- if (!raw) return null;
155
-
156
- switch (raw) {
157
- case "pending_question":
158
- case "tool_approval":
159
- case "tool_grant_request":
160
- case "access_request":
161
- return raw;
162
- default:
163
- return null;
164
- }
165
- }
166
-
167
- function parseBasePayload(
168
- payload: Record<string, unknown>,
169
- ): GuardianQuestionPayloadBase | null {
170
- const requestId = nonEmptyString(payload.requestId);
171
- const requestCode = nonEmptyString(payload.requestCode);
172
- const questionText = nonEmptyString(payload.questionText);
173
- if (!requestId || !requestCode || !questionText) return null;
174
- return { requestId, requestCode, questionText };
175
- }
211
+ // ── Payload parsing ────────────────────────────────────────────────────
176
212
 
177
213
  /**
178
- * Parse a guardian.question context payload into a strict discriminated union.
179
- *
180
- * Returns null when required fields for the declared requestKind are missing,
181
- * or when requestKind is absent/unknown.
214
+ * Parse a guardian.question context payload into a strict discriminated union
215
+ * using Zod validation. Returns null when the payload is missing required
216
+ * fields or has an unknown/missing requestKind.
182
217
  */
183
218
  export function parseGuardianQuestionPayload(
184
219
  payload: Record<string, unknown>,
185
220
  ): GuardianQuestionPayload | null {
186
- const requestKind = parseGuardianQuestionRequestKind(payload);
187
- if (!requestKind) return null;
188
-
189
- const base = parseBasePayload(payload);
190
- if (!base) return null;
191
-
192
- switch (requestKind) {
193
- case "pending_question": {
194
- const callSessionId = nonEmptyString(payload.callSessionId);
195
- const activeGuardianRequestCount =
196
- typeof payload.activeGuardianRequestCount === "number"
197
- ? payload.activeGuardianRequestCount
198
- : undefined;
199
- const toolName = nonEmptyString(payload.toolName);
200
- if (
201
- !callSessionId ||
202
- activeGuardianRequestCount === undefined ||
203
- Number.isNaN(activeGuardianRequestCount)
204
- ) {
205
- return null;
206
- }
207
- const pendingQuestionPayload: PendingQuestionGuardianPayload = {
208
- requestKind,
209
- ...base,
210
- callSessionId,
211
- activeGuardianRequestCount,
212
- };
213
- if (toolName) {
214
- pendingQuestionPayload.toolName = toolName;
215
- }
216
- return {
217
- ...pendingQuestionPayload,
218
- };
219
- }
220
- case "tool_approval":
221
- case "tool_grant_request": {
222
- const toolName = nonEmptyString(payload.toolName);
223
- if (!toolName) return null;
224
- return {
225
- requestKind,
226
- ...base,
227
- toolName,
228
- };
229
- }
230
- case "access_request":
231
- return {
232
- requestKind,
233
- ...base,
234
- };
235
- default:
236
- return null;
237
- }
221
+ const result = GuardianQuestionPayloadSchema.safeParse(payload);
222
+ return result.success ? result.data : null;
238
223
  }
239
224
 
240
225
  function resolveGuardianInstructionModeForRequestKind(
@@ -242,7 +227,7 @@ function resolveGuardianInstructionModeForRequestKind(
242
227
  toolName?: string | null,
243
228
  ): GuardianQuestionInstructionMode {
244
229
  const config = REQUEST_KIND_MODE_CONFIG[requestKind];
245
- const normalizedToolName = nonEmptyString(toolName);
230
+ const normalizedToolName = nonEmpty(toolName ?? undefined) ?? null;
246
231
  if (normalizedToolName && config.modeWhenToolNamePresent) {
247
232
  return config.modeWhenToolNamePresent;
248
233
  }
@@ -257,16 +242,14 @@ export function resolveGuardianInstructionModeFromFields(
257
242
  requestKind: GuardianQuestionRequestKind;
258
243
  mode: GuardianQuestionInstructionMode;
259
244
  } | null {
260
- const requestKind = parseGuardianQuestionRequestKind({
261
- requestKind: requestKindValue,
262
- });
263
- if (!requestKind) return null;
245
+ const parsed = GuardianQuestionRequestKindSchema.safeParse(requestKindValue);
246
+ if (!parsed.success) return null;
264
247
 
265
248
  return {
266
- requestKind,
249
+ requestKind: parsed.data,
267
250
  mode: resolveGuardianInstructionModeForRequestKind(
268
- requestKind,
269
- nonEmptyString(toolNameValue),
251
+ parsed.data,
252
+ typeof toolNameValue === "string" ? toolNameValue : null,
270
253
  ),
271
254
  };
272
255
  }
@@ -283,6 +266,23 @@ export function resolveGuardianInstructionModeForRequest(
283
266
  return modeResolution.mode;
284
267
  }
285
268
 
269
+ /**
270
+ * Resolve instruction mode directly from a typed guardian question payload.
271
+ * Avoids re-parsing when the caller already holds a validated payload.
272
+ */
273
+ export function resolveGuardianInstructionModeFromPayload(
274
+ payload: GuardianQuestionPayload,
275
+ ): GuardianQuestionModeResolution {
276
+ const toolName = "toolName" in payload ? payload.toolName : undefined;
277
+ return {
278
+ mode: resolveGuardianInstructionModeForRequestKind(
279
+ payload.requestKind,
280
+ toolName ?? null,
281
+ ),
282
+ requestKind: payload.requestKind,
283
+ };
284
+ }
285
+
286
286
  function getModeTextConfig(
287
287
  mode: GuardianQuestionInstructionMode,
288
288
  ): GuardianModeTextConfig {
@@ -420,28 +420,18 @@ export function stripConflictingGuardianRequestInstructions(
420
420
  }
421
421
 
422
422
  /**
423
- * Resolve guardian reply instruction mode from request kind.
423
+ * Resolve guardian reply instruction mode from a raw context payload.
424
424
  *
425
- * Requires a valid requestKind in the payload. When the payload cannot be
426
- * fully parsed as a typed guardian question, falls back to field-level
427
- * requestKind resolution. If requestKind is missing or unknown, defaults
428
- * to "approval" mode.
425
+ * Attempts Zod-based parsing first. When that fails, falls back to
426
+ * field-level requestKind resolution. Defaults to "approval" mode
427
+ * when requestKind is missing or unknown.
429
428
  */
430
429
  export function resolveGuardianQuestionInstructionMode(
431
430
  payload: Record<string, unknown>,
432
431
  ): GuardianQuestionModeResolution {
433
432
  const parsed = parseGuardianQuestionPayload(payload);
434
433
  if (parsed) {
435
- const parsedToolName = nonEmptyString(
436
- "toolName" in parsed ? parsed.toolName : null,
437
- );
438
- return {
439
- mode: resolveGuardianInstructionModeForRequestKind(
440
- parsed.requestKind,
441
- parsedToolName,
442
- ),
443
- requestKind: parsed.requestKind,
444
- };
434
+ return resolveGuardianInstructionModeFromPayload(parsed);
445
435
  }
446
436
 
447
437
  const requestKindResolution = resolveGuardianInstructionModeFromFields(
@@ -22,7 +22,7 @@ import { getConversation } from "../memory/conversation-crud.js";
22
22
  import { isBackgroundConversationType } from "../memory/conversation-types.js";
23
23
  import { getLogger } from "../util/logger.js";
24
24
  import { isConversationSeedSane } from "./conversation-seed-composer.js";
25
- import { readPayloadString } from "./copy-composer.js";
25
+ import { readPayloadString } from "./notification-utils.js";
26
26
  import type { NotificationSignal } from "./signal.js";
27
27
  import type { NotificationDecision, RenderedChannelCopy } from "./types.js";
28
28
 
@@ -58,10 +58,8 @@ export async function writeHomeFeedItemForSignal(
58
58
  decision: NotificationDecision,
59
59
  fallbackConversationId?: string,
60
60
  ): Promise<FeedItem | null> {
61
- const { mirror, sourceConversationId } = resolveHomeFeedMirror(
62
- signal,
63
- fallbackConversationId,
64
- );
61
+ const { mirror, sourceConversationId, sourceScheduleJobId } =
62
+ resolveHomeFeedMirror(signal, fallbackConversationId);
65
63
  if (!mirror) return null;
66
64
 
67
65
  const renderedCopy =
@@ -106,13 +104,28 @@ export async function writeHomeFeedItemForSignal(
106
104
 
107
105
  const category = deriveCategory(signal);
108
106
  const panelKind = deriveDetailPanelKind(signal);
109
- const metadata =
107
+
108
+ const baseMetadata =
110
109
  signal.contextPayload &&
111
110
  typeof signal.contextPayload === "object" &&
112
111
  !Array.isArray(signal.contextPayload)
113
- ? (signal.contextPayload as Record<string, unknown>)
112
+ ? { ...(signal.contextPayload as Record<string, unknown>) }
114
113
  : undefined;
115
114
 
115
+ // Link scheduled-run notifications back to their schedule. `notify`-mode
116
+ // jobs put `scheduleId` directly in the context payload; `execute`-mode (and
117
+ // other agent-backed) jobs only tag their conversation, so fall back to the
118
+ // source conversation's `scheduleJobId`.
119
+ const scheduleId =
120
+ readPayloadString(signal.contextPayload, "scheduleId") ??
121
+ sourceScheduleJobId ??
122
+ undefined;
123
+
124
+ const metadata =
125
+ scheduleId !== undefined
126
+ ? { ...(baseMetadata ?? {}), scheduleId }
127
+ : baseMetadata;
128
+
116
129
  const item: FeedItem = {
117
130
  id: `notif:${signal.signalId}`,
118
131
  type: "notification",
@@ -202,8 +215,11 @@ function resolveHomeFeedMirror(
202
215
  ): {
203
216
  mirror: boolean;
204
217
  sourceConversationId?: string;
218
+ sourceScheduleJobId?: string;
205
219
  } {
206
- let sourceRow: { conversationType?: string } | null = null;
220
+ let sourceRow:
221
+ | { conversationType?: string; scheduleJobId?: string | null }
222
+ | null = null;
207
223
  if (signal.sourceContextId) {
208
224
  try {
209
225
  sourceRow = getConversation(signal.sourceContextId) ?? null;
@@ -219,15 +235,16 @@ function resolveHomeFeedMirror(
219
235
  const sourceConversationId = sourceRow
220
236
  ? signal.sourceContextId
221
237
  : fallbackConversationId;
238
+ const sourceScheduleJobId = sourceRow?.scheduleJobId ?? undefined;
222
239
 
223
240
  if (signal.sourceChannel === "assistant_tool") {
224
- return { mirror: true, sourceConversationId };
241
+ return { mirror: true, sourceConversationId, sourceScheduleJobId };
225
242
  }
226
243
  if (signal.attentionHints.isAsyncBackground) {
227
- return { mirror: true, sourceConversationId };
244
+ return { mirror: true, sourceConversationId, sourceScheduleJobId };
228
245
  }
229
246
  if (isBackgroundConversationType(sourceRow?.conversationType)) {
230
- return { mirror: true, sourceConversationId };
247
+ return { mirror: true, sourceConversationId, sourceScheduleJobId };
231
248
  }
232
249
  return { mirror: false };
233
250
  }
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Shared leaf utilities for the notification pipeline.
3
+ *
4
+ * This module has no intra-pipeline imports — it depends only on Node
5
+ * stdlib — so any notification module can import it without creating
6
+ * circular dependencies.
7
+ */
8
+
9
+ // ── String helpers ──────────────────────────────────────────────────────────
10
+
11
+ /** Return `value` trimmed, or `undefined` when blank/nullish. */
12
+ export function nonEmpty(value: string | null | undefined): string | undefined {
13
+ if (typeof value !== "string") return undefined;
14
+ const trimmed = value.trim();
15
+ return trimmed.length > 0 ? trimmed : undefined;
16
+ }
17
+
18
+ /**
19
+ * Safely read a string property from an unknown-typed payload object.
20
+ * Returns `undefined` when the payload is falsy, not an object, or the
21
+ * key does not hold a string value.
22
+ */
23
+ export function readPayloadString(
24
+ payload: unknown,
25
+ key: string,
26
+ ): string | undefined {
27
+ if (!payload || typeof payload !== "object") return undefined;
28
+ const value = (payload as Record<string, unknown>)[key];
29
+ return typeof value === "string" ? value : undefined;
30
+ }
31
+
32
+ // ── Sanitization ────────────────────────────────────────────────────────────
33
+
34
+ const IDENTITY_FIELD_MAX_LENGTH = 120;
35
+
36
+ /**
37
+ * Sanitize an untrusted identity field for inclusion in notification copy.
38
+ *
39
+ * - Strips control characters (U+0000–U+001F, U+007F–U+009F) and newlines.
40
+ * - Clamps to 120 characters.
41
+ */
42
+ export function sanitizeIdentityField(value: string): string {
43
+ const stripped = value.replace(/[\x00-\x1f\x7f-\x9f\r\n]+/g, " ").trim();
44
+ const clamped =
45
+ stripped.length > IDENTITY_FIELD_MAX_LENGTH
46
+ ? stripped.slice(0, IDENTITY_FIELD_MAX_LENGTH) + "…"
47
+ : stripped;
48
+ return clamped;
49
+ }
50
+
51
+ export const MESSAGE_PREVIEW_MAX_LENGTH = 200;
52
+
53
+ /**
54
+ * Sanitize an untrusted message preview for inclusion in notification copy.
55
+ *
56
+ * Same as {@link sanitizeIdentityField} but uses a higher length limit
57
+ * (200 chars) suited for message previews.
58
+ */
59
+ export function sanitizeMessagePreview(value: string): string {
60
+ const stripped = value.replace(/[\x00-\x1f\x7f-\x9f\r\n]+/g, " ").trim();
61
+ const clamped =
62
+ stripped.length > MESSAGE_PREVIEW_MAX_LENGTH
63
+ ? stripped.slice(0, MESSAGE_PREVIEW_MAX_LENGTH) + "…"
64
+ : stripped;
65
+ return clamped;
66
+ }
@@ -159,6 +159,12 @@ export interface AccessRequestContextPayload {
159
159
  previousMemberStatus: string | null;
160
160
  /** Preview of the requester's original message (first ~200 chars). */
161
161
  messagePreview: string | null;
162
+ /** Slack-specific: user is from an external workspace (Slack Connect). */
163
+ isStranger?: boolean;
164
+ /** Slack-specific: user is a guest / restricted account. */
165
+ isRestricted?: boolean;
166
+ /** Slack message timestamp (e.g. "1234567890.123456") for permalink construction. */
167
+ messageTs?: string;
162
168
  }
163
169
 
164
170
  export interface GuardianChannelActivationPayload {
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Deterministic helpers for building guardian-facing tool-approval copy.
3
+ *
4
+ * Produces Surface card seed content blocks for `tool_approval`,
5
+ * `tool_grant_request`, and voice/call `pending_question` (with `toolName`)
6
+ * guardian questions, enabling Approve/Reject buttons in the Vellum in-app
7
+ * channel (web/macOS/iOS).
8
+ */
9
+
10
+ import { buildApprovalCardBlocks } from "./approval-card-builder.js";
11
+ import {
12
+ buildGuardianRequestCodeInstruction,
13
+ type GuardianQuestionPayload,
14
+ type LenientToolApprovalPayload,
15
+ LenientToolApprovalPayloadSchema,
16
+ parseGuardianQuestionPayload,
17
+ resolveGuardianInstructionModeFromFields,
18
+ resolveGuardianInstructionModeFromPayload,
19
+ } from "./guardian-question-mode.js";
20
+ import { nonEmpty, sanitizeIdentityField } from "./notification-utils.js";
21
+
22
+ // ── Approval detection ──────────────────────────────────────────────────────
23
+
24
+ /**
25
+ * Determine whether a typed guardian.question payload represents a tool
26
+ * approval (as opposed to a free-text answer). Uses the canonical mode
27
+ * resolver in `guardian-question-mode.ts` — the single source of truth
28
+ * for the requestKind → instructionMode mapping.
29
+ */
30
+ function isToolApprovalPayload(payload: GuardianQuestionPayload): boolean {
31
+ const { mode } = resolveGuardianInstructionModeFromPayload(payload);
32
+ return mode === "approval" && payload.requestKind !== "access_request";
33
+ }
34
+
35
+ /**
36
+ * Lenient approval detection for partially-constructed payloads that don't
37
+ * satisfy the strict discriminated union schema.
38
+ */
39
+ function isLenientToolApproval(payload: LenientToolApprovalPayload): boolean {
40
+ const modeResolution = resolveGuardianInstructionModeFromFields(
41
+ payload.requestKind,
42
+ payload.toolName,
43
+ );
44
+ if (!modeResolution) return false;
45
+ return (
46
+ modeResolution.mode === "approval" &&
47
+ payload.requestKind !== "access_request"
48
+ );
49
+ }
50
+
51
+ // ── Seed content blocks (Surface-based rendering) ───────────────────────────
52
+
53
+ /**
54
+ * Build structured content blocks for a tool approval/grant notification seed
55
+ * message. Returns `null` when the payload does not represent a tool approval.
56
+ *
57
+ * Accepts both strict `GuardianQuestionPayload` (Zod-validated) and raw
58
+ * `Record<string, unknown>` payloads. For raw payloads, attempts strict
59
+ * parsing first, then falls back to lenient field extraction so cards
60
+ * still render when optional fields are absent.
61
+ */
62
+ export function buildToolApprovalSeedContentBlocks(
63
+ payload: GuardianQuestionPayload,
64
+ ): unknown[] | null;
65
+ export function buildToolApprovalSeedContentBlocks(
66
+ payload: Record<string, unknown>,
67
+ ): unknown[] | null;
68
+ export function buildToolApprovalSeedContentBlocks(
69
+ payload: GuardianQuestionPayload | Record<string, unknown>,
70
+ ): unknown[] | null {
71
+ // Try strict Zod parsing first (full discriminated union).
72
+ const strict = parseGuardianQuestionPayload(
73
+ payload as Record<string, unknown>,
74
+ );
75
+ if (strict) {
76
+ if (!isToolApprovalPayload(strict)) return null;
77
+ return buildCardFromFields(strict);
78
+ }
79
+
80
+ // Fall back to lenient parsing — requires only `requestKind`.
81
+ const lenient = LenientToolApprovalPayloadSchema.safeParse(payload);
82
+ if (!lenient.success) return null;
83
+ if (!isLenientToolApproval(lenient.data)) return null;
84
+ return buildCardFromFields(lenient.data);
85
+ }
86
+
87
+ // ── Card construction (shared between strict and lenient paths) ─────────────
88
+
89
+ function buildCardFromFields(
90
+ p: GuardianQuestionPayload | LenientToolApprovalPayload,
91
+ ): unknown[] {
92
+ const toolName =
93
+ ("toolName" in p ? nonEmpty(p.toolName) : undefined) ?? "unknown tool";
94
+ const rawRequester = nonEmpty(p.requesterIdentifier);
95
+ const requester = rawRequester
96
+ ? sanitizeIdentityField(rawRequester)
97
+ : "Someone";
98
+
99
+ const isGrant = p.requestKind === "tool_grant_request";
100
+
101
+ const metadata: Array<{ label: string; value: string }> = [];
102
+ metadata.push({ label: "Tool", value: toolName });
103
+ const sourceChannel = nonEmpty(p.sourceChannel);
104
+ if (sourceChannel) {
105
+ metadata.push({ label: "Source", value: sourceChannel });
106
+ }
107
+
108
+ const body = p.questionText
109
+ ? `> ${p.questionText}`
110
+ : "No additional context available.";
111
+
112
+ // Fallback text with request-code instructions for older clients.
113
+ const baseFallback =
114
+ p.questionText ?? `${requester} is requesting approval to use ${toolName}`;
115
+ let fallbackText = baseFallback;
116
+ const requestCode = nonEmpty(p.requestCode);
117
+ if (requestCode) {
118
+ const modeResolution = resolveGuardianInstructionModeFromFields(
119
+ p.requestKind,
120
+ "toolName" in p ? (p.toolName ?? undefined) : undefined,
121
+ );
122
+ const mode = modeResolution?.mode ?? "approval";
123
+ const instruction = buildGuardianRequestCodeInstruction(
124
+ requestCode.trim().toUpperCase(),
125
+ mode,
126
+ );
127
+ fallbackText = `${baseFallback}\n\n${instruction}`;
128
+ }
129
+
130
+ return buildApprovalCardBlocks({
131
+ surfaceIdPrefix: "tool-approval",
132
+ cardTitle: isGrant ? "Tool Grant Request" : "Tool Approval",
133
+ requesterName: requester,
134
+ subtitle: isGrant
135
+ ? "Requesting permission to use this tool"
136
+ : "Requesting approval to run this tool",
137
+ body,
138
+ metadata,
139
+ requestId: nonEmpty(p.requestId),
140
+ fallbackText,
141
+ });
142
+ }