@agent-native/core 0.134.1 → 0.135.1

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 (463) hide show
  1. package/corpus/README.md +3 -3
  2. package/corpus/core/CHANGELOG.md +113 -0
  3. package/corpus/core/package.json +3 -2
  4. package/corpus/core/src/automations/service.ts +41 -4
  5. package/corpus/core/src/cli/mcp-config-writers.ts +27 -21
  6. package/corpus/core/src/client/agent-page/AgentJobsTab.tsx +187 -85
  7. package/corpus/core/src/client/agent-page/AutomationDetailsDialog.tsx +208 -0
  8. package/corpus/core/src/client/agent-page/AutomationScheduleDialog.tsx +180 -0
  9. package/corpus/core/src/client/agent-page/TimezoneSelect.tsx +241 -0
  10. package/corpus/core/src/client/agent-page/use-jobs.ts +50 -10
  11. package/corpus/core/src/client/org/OrgSwitcher.tsx +26 -0
  12. package/corpus/core/src/client/settings/AccountSettingsCard.tsx +35 -31
  13. package/corpus/core/src/client/settings/SchedulingTimezoneField.tsx +71 -0
  14. package/corpus/core/src/client/settings/SettingsRow.tsx +113 -0
  15. package/corpus/core/src/client/settings/agent-settings-search.ts +2 -1
  16. package/corpus/core/src/client/settings/index.ts +6 -0
  17. package/corpus/core/src/integrations/identity.ts +2 -12
  18. package/corpus/core/src/jobs/actions/list-automation-runs.ts +33 -0
  19. package/corpus/core/src/jobs/actions/list-recurring-jobs.ts +27 -6
  20. package/corpus/core/src/jobs/actions/manage-recurring-job.ts +41 -9
  21. package/corpus/core/src/jobs/background-automation-runner.ts +88 -0
  22. package/corpus/core/src/jobs/cron.ts +52 -6
  23. package/corpus/core/src/jobs/frontmatter.ts +20 -0
  24. package/corpus/core/src/jobs/run-history.ts +242 -0
  25. package/corpus/core/src/jobs/scheduler.ts +73 -18
  26. package/corpus/core/src/jobs/tools.ts +54 -6
  27. package/corpus/core/src/localization/actions/get-localization-preference.ts +2 -2
  28. package/corpus/core/src/localization/actions/set-localization-preference.ts +49 -11
  29. package/corpus/core/src/localization/default-messages.ts +27 -0
  30. package/corpus/core/src/localization/shared.ts +39 -6
  31. package/corpus/core/src/localization/user-timezone.ts +36 -0
  32. package/corpus/core/src/org/app-roles-handlers.ts +1 -9
  33. package/corpus/core/src/org/index.ts +2 -0
  34. package/corpus/core/src/org/membership.ts +18 -0
  35. package/corpus/core/src/review/actions/create-review-comment.ts +4 -1
  36. package/corpus/core/src/review/actions/reply-review-comment.ts +4 -1
  37. package/corpus/core/src/review/index.ts +5 -0
  38. package/corpus/core/src/review/notifications.ts +163 -0
  39. package/corpus/core/src/review/types.ts +7 -0
  40. package/corpus/core/src/server/action-discovery.ts +4 -0
  41. package/corpus/core/src/server/activity-notifications.ts +174 -0
  42. package/corpus/core/src/server/app-name.ts +31 -0
  43. package/corpus/core/src/server/better-auth-instance.ts +4 -4
  44. package/corpus/core/src/server/email-templates.ts +70 -14
  45. package/corpus/core/src/server/email.ts +66 -3
  46. package/corpus/core/src/server/index.ts +10 -0
  47. package/corpus/core/src/sharing/access.ts +44 -2
  48. package/corpus/core/src/sharing/index.ts +5 -0
  49. package/corpus/core/src/sharing/recipients.ts +67 -0
  50. package/corpus/core/src/triggers/actions/list-automations.ts +27 -8
  51. package/corpus/core/src/triggers/actions/manage-automation.ts +17 -6
  52. package/corpus/core/src/triggers/actions.ts +16 -4
  53. package/corpus/core/src/triggers/dispatcher.ts +44 -30
  54. package/corpus/core/src/triggers/routes.ts +19 -6
  55. package/corpus/core/src/vitest-config.ts +68 -0
  56. package/corpus/core/vitest.config.ts +5 -0
  57. package/corpus/templates/analytics/app/pages/Settings.tsx +37 -53
  58. package/corpus/templates/analytics/changelog/2026-08-01-new-error-alerts-now-reach-you-by-email-and-a-scheduled-dash.md +6 -0
  59. package/corpus/templates/analytics/server/jobs/dashboard-report.ts +43 -0
  60. package/corpus/templates/analytics/server/lib/error-capture.ts +5 -1
  61. package/corpus/templates/analytics/vitest.config.ts +16 -12
  62. package/corpus/templates/assets/actions/refresh-generation-run.ts +3 -0
  63. package/corpus/templates/assets/app/components/ui/switch.tsx +1 -0
  64. package/corpus/templates/assets/app/hooks/use-assets-prefs.ts +55 -0
  65. package/corpus/templates/assets/app/i18n-data.ts +30 -0
  66. package/corpus/templates/assets/app/routes/settings.tsx +58 -55
  67. package/corpus/templates/assets/changelog/2026-08-01-get-an-email-when-a-generation-you-started-finishes-or-fails.md +6 -0
  68. package/corpus/templates/assets/server/lib/generation-run-notifications.ts +89 -0
  69. package/corpus/templates/assets/server/lib/video-runs.ts +8 -1
  70. package/corpus/templates/assets/server/routes/_agent-native/assets/user-prefs.get.ts +17 -0
  71. package/corpus/templates/assets/server/routes/_agent-native/assets/user-prefs.put.ts +36 -0
  72. package/corpus/templates/assets/shared/assets-user-prefs.ts +10 -0
  73. package/corpus/templates/assets/vitest.config.ts +16 -12
  74. package/corpus/templates/brain/app/routes/settings.tsx +15 -15
  75. package/corpus/templates/brain/vitest.config.ts +16 -12
  76. package/corpus/templates/calendar/app/pages/Settings.tsx +32 -40
  77. package/corpus/templates/calendar/changelog/2026-08-01-language-and-appearance-now-sit-together-in-one-preferences-.md +6 -0
  78. package/corpus/templates/calendar/vitest.config.ts +16 -12
  79. package/corpus/templates/chat/app/routes/settings.tsx +14 -22
  80. package/corpus/templates/chat/vitest.config.ts +9 -0
  81. package/corpus/templates/clips/.agents/skills/video-editing/SKILL.md +31 -0
  82. package/corpus/templates/clips/AGENTS.md +1 -0
  83. package/corpus/templates/clips/actions/add-comment.ts +12 -1
  84. package/corpus/templates/clips/actions/create-meeting.ts +2 -2
  85. package/corpus/templates/clips/actions/create-recording.ts +2 -2
  86. package/corpus/templates/clips/actions/finalize-recording.ts +42 -6
  87. package/corpus/templates/clips/actions/generate-filmstrip.ts +154 -0
  88. package/corpus/templates/clips/actions/get-recording-player-data.ts +6 -0
  89. package/corpus/templates/clips/actions/import-loom-recording.ts +2 -2
  90. package/corpus/templates/clips/actions/lib/ensure-recording-filmstrip.ts +258 -0
  91. package/corpus/templates/clips/actions/lib/ensure-seekable-video.ts +11 -3
  92. package/corpus/templates/clips/actions/react-to-comment.ts +13 -0
  93. package/corpus/templates/clips/actions/react-to-recording.ts +10 -1
  94. package/corpus/templates/clips/actions/reply-to-comment.ts +12 -1
  95. package/corpus/templates/clips/actions/stitch-recordings.ts +2 -2
  96. package/corpus/templates/clips/app/components/editor/editor-layout.tsx +152 -23
  97. package/corpus/templates/clips/app/components/editor/waveform.tsx +237 -75
  98. package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +23 -1
  99. package/corpus/templates/clips/app/components/settings/ai-setup-section.tsx +441 -0
  100. package/corpus/templates/clips/app/components/settings/slack-section.tsx +329 -0
  101. package/corpus/templates/clips/app/components/settings/types.ts +14 -0
  102. package/corpus/templates/clips/app/components/settings/video-storage-section.tsx +443 -0
  103. package/corpus/templates/clips/app/hooks/use-secret-status.ts +69 -0
  104. package/corpus/templates/clips/app/i18n/en-US.ts +9 -1
  105. package/corpus/templates/clips/app/lib/video-filmstrip.ts +262 -0
  106. package/corpus/templates/clips/app/routes/_app.settings._index.tsx +165 -1351
  107. package/corpus/templates/clips/changelog/2026-07-27-saved-clips-now-resume-from-the-last-uploaded-byte-after-an-.md +6 -0
  108. package/corpus/templates/clips/changelog/2026-07-31-editor-timeline-now-shows-video-frames-behind-the-waveform-g.md +6 -0
  109. package/corpus/templates/clips/changelog/2026-07-31-email-notifications-now-go-out-when-someone-comments-on-or-r.md +6 -0
  110. package/corpus/templates/clips/changelog/2026-07-31-you-can-now-set-a-default-visibility-for-new-recordings-in-s.md +6 -0
  111. package/corpus/templates/clips/changelog/2026-08-01-email-notifications-for-comments-and-reactions-now-actually-.md +6 -0
  112. package/corpus/templates/clips/changelog/2026-08-03-the-desktop-app-now-opens-directly-beneath-its-tray-icon-on-.md +6 -0
  113. package/corpus/templates/clips/chrome-extension/package.json +1 -0
  114. package/corpus/templates/clips/chrome-extension/vitest.config.ts +9 -0
  115. package/corpus/templates/clips/desktop/package.json +1 -0
  116. package/corpus/templates/clips/desktop/src/app.tsx +17 -1
  117. package/corpus/templates/clips/desktop/src/lib/recorder.ts +331 -36
  118. package/corpus/templates/clips/desktop/src/lib/upload-recovery.ts +129 -0
  119. package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +815 -15
  120. package/corpus/templates/clips/desktop/src-tauri/src/tray.rs +61 -11
  121. package/corpus/templates/clips/desktop/vitest.config.ts +9 -0
  122. package/corpus/templates/clips/server/db/schema.ts +17 -0
  123. package/corpus/templates/clips/server/jobs/media-verification.ts +4 -0
  124. package/corpus/templates/clips/server/lib/activity-notifications.ts +185 -0
  125. package/corpus/templates/clips/server/lib/recording-media-cleanup.ts +2 -0
  126. package/corpus/templates/clips/server/lib/recording-upload-state.ts +46 -9
  127. package/corpus/templates/clips/server/lib/recordings.ts +24 -0
  128. package/corpus/templates/clips/server/lib/resumable-session.ts +10 -4
  129. package/corpus/templates/clips/server/lib/transactional-email-templates.ts +109 -0
  130. package/corpus/templates/clips/server/lib/upload-interruption.ts +8 -0
  131. package/corpus/templates/clips/server/lib/upload-lease.ts +50 -4
  132. package/corpus/templates/clips/server/lib/video-filmstrip-sprite.ts +207 -0
  133. package/corpus/templates/clips/server/lib/video-remux.ts +13 -4
  134. package/corpus/templates/clips/server/plugins/db.ts +16 -0
  135. package/corpus/templates/clips/server/routes/api/_agent-native-background/post-finalize-worker.post.ts +4 -0
  136. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/abort.post.ts +123 -36
  137. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/chunk.post.ts +159 -29
  138. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/interrupt.post.ts +197 -0
  139. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/reset-chunks.post.ts +193 -39
  140. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/resume.get.ts +191 -12
  141. package/corpus/templates/clips/shared/clips-ai-prefs.ts +6 -0
  142. package/corpus/templates/clips/shared/feature-flags.ts +12 -0
  143. package/corpus/templates/clips/shared/recording-core.ts +4 -0
  144. package/corpus/templates/clips/vitest.config.ts +23 -19
  145. package/corpus/templates/content/actions/_database-utils.ts +113 -4
  146. package/corpus/templates/content/actions/add-comment.ts +15 -1
  147. package/corpus/templates/content/actions/get-document.ts +84 -3
  148. package/corpus/templates/content/actions/list-documents.ts +1 -0
  149. package/corpus/templates/content/app/components/editor/BuilderBodySyncingNotice.tsx +9 -2
  150. package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +34 -15
  151. package/corpus/templates/content/app/components/editor/DocumentToolbar.tsx +55 -13
  152. package/corpus/templates/content/app/components/editor/body-hydration.ts +12 -6
  153. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +2 -3
  154. package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +5 -12
  155. package/corpus/templates/content/app/components/ui/switch.tsx +1 -0
  156. package/corpus/templates/content/app/hooks/use-content-database.ts +16 -27
  157. package/corpus/templates/content/app/hooks/use-content-prefs.ts +55 -0
  158. package/corpus/templates/content/app/hooks/use-create-page.ts +3 -6
  159. package/corpus/templates/content/app/hooks/use-document-properties.ts +9 -16
  160. package/corpus/templates/content/app/hooks/use-document-versions.ts +3 -3
  161. package/corpus/templates/content/app/hooks/use-documents.ts +85 -29
  162. package/corpus/templates/content/app/hooks/use-notion.ts +15 -13
  163. package/corpus/templates/content/app/i18n-data.ts +72 -1
  164. package/corpus/templates/content/app/lib/document-query.ts +36 -0
  165. package/corpus/templates/content/app/routes/_app.settings.tsx +47 -18
  166. package/corpus/templates/content/changelog/2026-07-30-breadcrumb-menus-now-hide-internal-system-pages.md +6 -0
  167. package/corpus/templates/content/changelog/2026-08-01-get-an-email-when-someone-comments-on-replies-in-or-mentions.md +6 -0
  168. package/corpus/templates/content/changelog/2026-08-02-pages-opened-from-a-database-now-keep-that-database-s-fields.md +6 -0
  169. package/corpus/templates/content/server/lib/comment-notifications.ts +146 -0
  170. package/corpus/templates/content/server/lib/document-context.ts +11 -6
  171. package/corpus/templates/content/server/routes/_agent-native/content/user-prefs.get.ts +17 -0
  172. package/corpus/templates/content/server/routes/_agent-native/content/user-prefs.put.ts +36 -0
  173. package/corpus/templates/content/shared/api.ts +8 -0
  174. package/corpus/templates/content/shared/content-user-prefs.ts +11 -0
  175. package/corpus/templates/content/vitest.config.ts +20 -16
  176. package/corpus/templates/crm/app/routes/settings.tsx +14 -22
  177. package/corpus/templates/crm/vitest.config.ts +8 -4
  178. package/corpus/templates/design/app/routes/settings.tsx +14 -22
  179. package/corpus/templates/design/changelog/2026-08-01-review-comments-replies-and-mentions-now-send-an-email-to-th.md +6 -0
  180. package/corpus/templates/design/vitest.config.ts +18 -12
  181. package/corpus/templates/dispatch/app/routes/settings.tsx +26 -40
  182. package/corpus/templates/dispatch/vitest.config.ts +11 -7
  183. package/corpus/templates/factory/app/routes/settings.tsx +14 -22
  184. package/corpus/templates/factory/vitest.config.ts +9 -0
  185. package/corpus/templates/forms/app/routes/_app.settings.tsx +14 -22
  186. package/corpus/templates/forms/vitest.config.ts +27 -23
  187. package/corpus/templates/macros/app/routes/settings.tsx +14 -22
  188. package/corpus/templates/macros/vitest.config.ts +16 -12
  189. package/corpus/templates/mail/app/pages/SettingsPage.tsx +14 -16
  190. package/corpus/templates/mail/vitest.config.ts +16 -12
  191. package/corpus/templates/plan/app/routes/settings.tsx +30 -44
  192. package/corpus/templates/plan/vitest.config.ts +24 -20
  193. package/corpus/templates/slides/actions/add-slide-comment.ts +12 -1
  194. package/corpus/templates/slides/app/components/ui/switch.tsx +1 -0
  195. package/corpus/templates/slides/app/hooks/use-slides-prefs.ts +55 -0
  196. package/corpus/templates/slides/app/i18n/en-US.ts +4 -0
  197. package/corpus/templates/slides/app/routes/settings.tsx +45 -22
  198. package/corpus/templates/slides/changelog/2026-08-01-get-an-email-when-someone-comments-on-or-replies-in-your-dec.md +6 -0
  199. package/corpus/templates/slides/server/lib/comment-notifications.ts +182 -0
  200. package/corpus/templates/slides/server/routes/_agent-native/slides/user-prefs.get.ts +16 -0
  201. package/corpus/templates/slides/server/routes/_agent-native/slides/user-prefs.put.ts +36 -0
  202. package/corpus/templates/slides/shared/slides-user-prefs.ts +11 -0
  203. package/corpus/templates/slides/vitest.config.ts +17 -13
  204. package/corpus/templates/tasks/app/components/layout/Sidebar.tsx +2 -0
  205. package/corpus/templates/tasks/app/routes/settings.tsx +14 -22
  206. package/corpus/templates/tasks/vitest.config.ts +17 -13
  207. package/corpus/toolkit/CHANGELOG.md +9 -0
  208. package/corpus/toolkit/package.json +1 -1
  209. package/corpus/toolkit/vitest.config.ts +3 -0
  210. package/dist/automations/service.d.ts +2 -0
  211. package/dist/automations/service.d.ts.map +1 -1
  212. package/dist/automations/service.js +29 -4
  213. package/dist/automations/service.js.map +1 -1
  214. package/dist/cli/mcp-config-writers.d.ts.map +1 -1
  215. package/dist/cli/mcp-config-writers.js +24 -23
  216. package/dist/cli/mcp-config-writers.js.map +1 -1
  217. package/dist/client/agent-page/AgentJobsTab.d.ts.map +1 -1
  218. package/dist/client/agent-page/AgentJobsTab.js +86 -29
  219. package/dist/client/agent-page/AgentJobsTab.js.map +1 -1
  220. package/dist/client/agent-page/AutomationDetailsDialog.d.ts +21 -0
  221. package/dist/client/agent-page/AutomationDetailsDialog.d.ts.map +1 -0
  222. package/dist/client/agent-page/AutomationDetailsDialog.js +33 -0
  223. package/dist/client/agent-page/AutomationDetailsDialog.js.map +1 -0
  224. package/dist/client/agent-page/AutomationScheduleDialog.d.ts +15 -0
  225. package/dist/client/agent-page/AutomationScheduleDialog.d.ts.map +1 -0
  226. package/dist/client/agent-page/AutomationScheduleDialog.js +46 -0
  227. package/dist/client/agent-page/AutomationScheduleDialog.js.map +1 -0
  228. package/dist/client/agent-page/TimezoneSelect.d.ts +17 -0
  229. package/dist/client/agent-page/TimezoneSelect.d.ts.map +1 -0
  230. package/dist/client/agent-page/TimezoneSelect.js +107 -0
  231. package/dist/client/agent-page/TimezoneSelect.js.map +1 -0
  232. package/dist/client/agent-page/use-jobs.d.ts +20 -7
  233. package/dist/client/agent-page/use-jobs.d.ts.map +1 -1
  234. package/dist/client/agent-page/use-jobs.js +24 -4
  235. package/dist/client/agent-page/use-jobs.js.map +1 -1
  236. package/dist/client/org/OrgSwitcher.d.ts +8 -1
  237. package/dist/client/org/OrgSwitcher.d.ts.map +1 -1
  238. package/dist/client/org/OrgSwitcher.js +8 -2
  239. package/dist/client/org/OrgSwitcher.js.map +1 -1
  240. package/dist/client/settings/AccountSettingsCard.d.ts.map +1 -1
  241. package/dist/client/settings/AccountSettingsCard.js +10 -9
  242. package/dist/client/settings/AccountSettingsCard.js.map +1 -1
  243. package/dist/client/settings/SchedulingTimezoneField.d.ts +10 -0
  244. package/dist/client/settings/SchedulingTimezoneField.d.ts.map +1 -0
  245. package/dist/client/settings/SchedulingTimezoneField.js +30 -0
  246. package/dist/client/settings/SchedulingTimezoneField.js.map +1 -0
  247. package/dist/client/settings/SettingsRow.d.ts +36 -0
  248. package/dist/client/settings/SettingsRow.d.ts.map +1 -0
  249. package/dist/client/settings/SettingsRow.js +15 -0
  250. package/dist/client/settings/SettingsRow.js.map +1 -0
  251. package/dist/client/settings/agent-settings-search.d.ts.map +1 -1
  252. package/dist/client/settings/agent-settings-search.js +1 -1
  253. package/dist/client/settings/agent-settings-search.js.map +1 -1
  254. package/dist/client/settings/index.d.ts +1 -0
  255. package/dist/client/settings/index.d.ts.map +1 -1
  256. package/dist/client/settings/index.js +1 -0
  257. package/dist/client/settings/index.js.map +1 -1
  258. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  259. package/dist/integrations/identity.d.ts.map +1 -1
  260. package/dist/integrations/identity.js +2 -11
  261. package/dist/integrations/identity.js.map +1 -1
  262. package/dist/jobs/actions/list-automation-runs.d.ts +8 -0
  263. package/dist/jobs/actions/list-automation-runs.d.ts.map +1 -0
  264. package/dist/jobs/actions/list-automation-runs.js +29 -0
  265. package/dist/jobs/actions/list-automation-runs.js.map +1 -0
  266. package/dist/jobs/actions/list-recurring-jobs.d.ts +2 -0
  267. package/dist/jobs/actions/list-recurring-jobs.d.ts.map +1 -1
  268. package/dist/jobs/actions/list-recurring-jobs.js +20 -7
  269. package/dist/jobs/actions/list-recurring-jobs.js.map +1 -1
  270. package/dist/jobs/actions/manage-recurring-job.d.ts +2 -0
  271. package/dist/jobs/actions/manage-recurring-job.js +30 -9
  272. package/dist/jobs/actions/manage-recurring-job.js.map +1 -1
  273. package/dist/jobs/background-automation-runner.d.ts.map +1 -1
  274. package/dist/jobs/background-automation-runner.js +60 -0
  275. package/dist/jobs/background-automation-runner.js.map +1 -1
  276. package/dist/jobs/cron.d.ts +16 -2
  277. package/dist/jobs/cron.d.ts.map +1 -1
  278. package/dist/jobs/cron.js +43 -6
  279. package/dist/jobs/cron.js.map +1 -1
  280. package/dist/jobs/frontmatter.d.ts +12 -0
  281. package/dist/jobs/frontmatter.d.ts.map +1 -1
  282. package/dist/jobs/frontmatter.js +8 -0
  283. package/dist/jobs/frontmatter.js.map +1 -1
  284. package/dist/jobs/run-history.d.ts +56 -0
  285. package/dist/jobs/run-history.d.ts.map +1 -0
  286. package/dist/jobs/run-history.js +169 -0
  287. package/dist/jobs/run-history.js.map +1 -0
  288. package/dist/jobs/scheduler.d.ts.map +1 -1
  289. package/dist/jobs/scheduler.js +51 -18
  290. package/dist/jobs/scheduler.js.map +1 -1
  291. package/dist/jobs/tools.d.ts.map +1 -1
  292. package/dist/jobs/tools.js +37 -6
  293. package/dist/jobs/tools.js.map +1 -1
  294. package/dist/localization/actions/get-localization-preference.d.ts +1 -2
  295. package/dist/localization/actions/get-localization-preference.d.ts.map +1 -1
  296. package/dist/localization/actions/get-localization-preference.js.map +1 -1
  297. package/dist/localization/actions/set-localization-preference.d.ts +3 -3
  298. package/dist/localization/actions/set-localization-preference.d.ts.map +1 -1
  299. package/dist/localization/actions/set-localization-preference.js +33 -8
  300. package/dist/localization/actions/set-localization-preference.js.map +1 -1
  301. package/dist/localization/default-messages.d.ts +25 -0
  302. package/dist/localization/default-messages.d.ts.map +1 -1
  303. package/dist/localization/default-messages.js +25 -0
  304. package/dist/localization/default-messages.js.map +1 -1
  305. package/dist/localization/shared.d.ts +16 -1
  306. package/dist/localization/shared.d.ts.map +1 -1
  307. package/dist/localization/shared.js +22 -4
  308. package/dist/localization/shared.js.map +1 -1
  309. package/dist/localization/user-timezone.d.ts +11 -0
  310. package/dist/localization/user-timezone.d.ts.map +1 -0
  311. package/dist/localization/user-timezone.js +31 -0
  312. package/dist/localization/user-timezone.js.map +1 -0
  313. package/dist/notifications/routes.d.ts +4 -4
  314. package/dist/observability/routes.d.ts +3 -3
  315. package/dist/org/app-roles-handlers.d.ts.map +1 -1
  316. package/dist/org/app-roles-handlers.js +1 -8
  317. package/dist/org/app-roles-handlers.js.map +1 -1
  318. package/dist/org/index.d.ts +1 -0
  319. package/dist/org/index.d.ts.map +1 -1
  320. package/dist/org/index.js +1 -0
  321. package/dist/org/index.js.map +1 -1
  322. package/dist/org/membership.d.ts +6 -0
  323. package/dist/org/membership.d.ts.map +1 -0
  324. package/dist/org/membership.js +16 -0
  325. package/dist/org/membership.js.map +1 -0
  326. package/dist/progress/routes.d.ts +1 -1
  327. package/dist/provider-api/actions/custom-provider-registration.d.ts +13 -13
  328. package/dist/provider-api/actions/provider-api.d.ts +8 -8
  329. package/dist/provider-api/corpus-jobs.d.ts +2 -2
  330. package/dist/review/actions/create-review-comment.d.ts +32 -1
  331. package/dist/review/actions/create-review-comment.d.ts.map +1 -1
  332. package/dist/review/actions/create-review-comment.js +3 -1
  333. package/dist/review/actions/create-review-comment.js.map +1 -1
  334. package/dist/review/actions/reply-review-comment.d.ts +32 -1
  335. package/dist/review/actions/reply-review-comment.d.ts.map +1 -1
  336. package/dist/review/actions/reply-review-comment.js +3 -1
  337. package/dist/review/actions/reply-review-comment.js.map +1 -1
  338. package/dist/review/index.d.ts +1 -0
  339. package/dist/review/index.d.ts.map +1 -1
  340. package/dist/review/index.js +1 -0
  341. package/dist/review/index.js.map +1 -1
  342. package/dist/review/notifications.d.ts +24 -0
  343. package/dist/review/notifications.d.ts.map +1 -0
  344. package/dist/review/notifications.js +123 -0
  345. package/dist/review/notifications.js.map +1 -0
  346. package/dist/review/types.d.ts +5 -0
  347. package/dist/review/types.d.ts.map +1 -1
  348. package/dist/review/types.js.map +1 -1
  349. package/dist/server/action-discovery.d.ts.map +1 -1
  350. package/dist/server/action-discovery.js +4 -0
  351. package/dist/server/action-discovery.js.map +1 -1
  352. package/dist/server/activity-notifications.d.ts +61 -0
  353. package/dist/server/activity-notifications.d.ts.map +1 -0
  354. package/dist/server/activity-notifications.js +103 -0
  355. package/dist/server/activity-notifications.js.map +1 -0
  356. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  357. package/dist/server/app-name.d.ts +18 -0
  358. package/dist/server/app-name.d.ts.map +1 -1
  359. package/dist/server/app-name.js +31 -0
  360. package/dist/server/app-name.js.map +1 -1
  361. package/dist/server/better-auth-instance.js +4 -4
  362. package/dist/server/better-auth-instance.js.map +1 -1
  363. package/dist/server/email-templates.d.ts +12 -0
  364. package/dist/server/email-templates.d.ts.map +1 -1
  365. package/dist/server/email-templates.js +57 -14
  366. package/dist/server/email-templates.js.map +1 -1
  367. package/dist/server/email.d.ts +12 -0
  368. package/dist/server/email.d.ts.map +1 -1
  369. package/dist/server/email.js +52 -5
  370. package/dist/server/email.js.map +1 -1
  371. package/dist/server/index.d.ts +1 -0
  372. package/dist/server/index.d.ts.map +1 -1
  373. package/dist/server/index.js +1 -0
  374. package/dist/server/index.js.map +1 -1
  375. package/dist/server/transcribe-voice.d.ts +1 -1
  376. package/dist/sharing/access.d.ts.map +1 -1
  377. package/dist/sharing/access.js +32 -2
  378. package/dist/sharing/access.js.map +1 -1
  379. package/dist/sharing/index.d.ts +1 -0
  380. package/dist/sharing/index.d.ts.map +1 -1
  381. package/dist/sharing/index.js +1 -0
  382. package/dist/sharing/index.js.map +1 -1
  383. package/dist/sharing/recipients.d.ts +31 -0
  384. package/dist/sharing/recipients.d.ts.map +1 -0
  385. package/dist/sharing/recipients.js +40 -0
  386. package/dist/sharing/recipients.js.map +1 -0
  387. package/dist/templates/chat/app/routes/settings.tsx +14 -22
  388. package/dist/templates/chat/vitest.config.ts +9 -0
  389. package/dist/triggers/actions/list-automations.d.ts +2 -0
  390. package/dist/triggers/actions/list-automations.d.ts.map +1 -1
  391. package/dist/triggers/actions/list-automations.js +19 -8
  392. package/dist/triggers/actions/list-automations.js.map +1 -1
  393. package/dist/triggers/actions/manage-automation.d.ts +6 -0
  394. package/dist/triggers/actions/manage-automation.js +12 -6
  395. package/dist/triggers/actions/manage-automation.js.map +1 -1
  396. package/dist/triggers/actions.d.ts.map +1 -1
  397. package/dist/triggers/actions.js +15 -4
  398. package/dist/triggers/actions.js.map +1 -1
  399. package/dist/triggers/dispatcher.d.ts.map +1 -1
  400. package/dist/triggers/dispatcher.js +19 -17
  401. package/dist/triggers/dispatcher.js.map +1 -1
  402. package/dist/triggers/routes.d.ts.map +1 -1
  403. package/dist/triggers/routes.js +6 -6
  404. package/dist/triggers/routes.js.map +1 -1
  405. package/dist/vitest-config.d.ts +10 -0
  406. package/dist/vitest-config.d.ts.map +1 -0
  407. package/dist/vitest-config.js +55 -0
  408. package/dist/vitest-config.js.map +1 -0
  409. package/package.json +5 -4
  410. package/src/automations/service.ts +41 -4
  411. package/src/cli/mcp-config-writers.ts +27 -21
  412. package/src/client/agent-page/AgentJobsTab.tsx +187 -85
  413. package/src/client/agent-page/AutomationDetailsDialog.tsx +208 -0
  414. package/src/client/agent-page/AutomationScheduleDialog.tsx +180 -0
  415. package/src/client/agent-page/TimezoneSelect.tsx +241 -0
  416. package/src/client/agent-page/use-jobs.ts +50 -10
  417. package/src/client/org/OrgSwitcher.tsx +26 -0
  418. package/src/client/settings/AccountSettingsCard.tsx +35 -31
  419. package/src/client/settings/SchedulingTimezoneField.tsx +71 -0
  420. package/src/client/settings/SettingsRow.tsx +113 -0
  421. package/src/client/settings/agent-settings-search.ts +2 -1
  422. package/src/client/settings/index.ts +6 -0
  423. package/src/integrations/identity.ts +2 -12
  424. package/src/jobs/actions/list-automation-runs.ts +33 -0
  425. package/src/jobs/actions/list-recurring-jobs.ts +27 -6
  426. package/src/jobs/actions/manage-recurring-job.ts +41 -9
  427. package/src/jobs/background-automation-runner.ts +88 -0
  428. package/src/jobs/cron.ts +52 -6
  429. package/src/jobs/frontmatter.ts +20 -0
  430. package/src/jobs/run-history.ts +242 -0
  431. package/src/jobs/scheduler.ts +73 -18
  432. package/src/jobs/tools.ts +54 -6
  433. package/src/localization/actions/get-localization-preference.ts +2 -2
  434. package/src/localization/actions/set-localization-preference.ts +49 -11
  435. package/src/localization/default-messages.ts +27 -0
  436. package/src/localization/shared.ts +39 -6
  437. package/src/localization/user-timezone.ts +36 -0
  438. package/src/org/app-roles-handlers.ts +1 -9
  439. package/src/org/index.ts +2 -0
  440. package/src/org/membership.ts +18 -0
  441. package/src/review/actions/create-review-comment.ts +4 -1
  442. package/src/review/actions/reply-review-comment.ts +4 -1
  443. package/src/review/index.ts +5 -0
  444. package/src/review/notifications.ts +163 -0
  445. package/src/review/types.ts +7 -0
  446. package/src/server/action-discovery.ts +4 -0
  447. package/src/server/activity-notifications.ts +174 -0
  448. package/src/server/app-name.ts +31 -0
  449. package/src/server/better-auth-instance.ts +4 -4
  450. package/src/server/email-templates.ts +70 -14
  451. package/src/server/email.ts +66 -3
  452. package/src/server/index.ts +10 -0
  453. package/src/sharing/access.ts +44 -2
  454. package/src/sharing/index.ts +5 -0
  455. package/src/sharing/recipients.ts +67 -0
  456. package/src/templates/chat/app/routes/settings.tsx +14 -22
  457. package/src/templates/chat/vitest.config.ts +9 -0
  458. package/src/triggers/actions/list-automations.ts +27 -8
  459. package/src/triggers/actions/manage-automation.ts +17 -6
  460. package/src/triggers/actions.ts +16 -4
  461. package/src/triggers/dispatcher.ts +44 -30
  462. package/src/triggers/routes.ts +19 -6
  463. package/src/vitest-config.ts +68 -0
@@ -0,0 +1,242 @@
1
+ import { randomUUID } from "node:crypto";
2
+
3
+ import { getDbExec, intType, isPostgres } from "../db/client.js";
4
+ import { ensureIndexExists, ensureTableExists } from "../db/ddl-guard.js";
5
+
6
+ /**
7
+ * "interrupted" is derived at read time, never stored: a process killed
8
+ * mid-run cannot write its own outcome, so a row left running long past the
9
+ * point a run could still be alive is reported as interrupted rather than
10
+ * shown as permanently in-flight.
11
+ */
12
+ export type AutomationRunStatus =
13
+ | "running"
14
+ | "success"
15
+ | "error"
16
+ | "interrupted";
17
+
18
+ export interface AutomationRun {
19
+ id: string;
20
+ owner: string;
21
+ automation: string;
22
+ path: string;
23
+ scope: string | null;
24
+ orgId: string | null;
25
+ runId: string | null;
26
+ threadId: string | null;
27
+ status: AutomationRunStatus;
28
+ startedAt: number;
29
+ finishedAt: number | null;
30
+ error: string | null;
31
+ }
32
+
33
+ export interface StartAutomationRunInput {
34
+ owner: string;
35
+ automation: string;
36
+ path: string;
37
+ scope?: string | null;
38
+ orgId?: string | null;
39
+ runId?: string | null;
40
+ threadId?: string | null;
41
+ }
42
+
43
+ const TABLE = "automation_runs";
44
+ const MAX_ERROR_LENGTH = 500;
45
+
46
+ /**
47
+ * Generous multiple of the runner's own 5 minute hard abort
48
+ * (BACKGROUND_RUN_HARD_TIMEOUT_MS). Past this, no run is still alive.
49
+ */
50
+ const RUN_LIVENESS_CEILING_MS = 15 * 60_000;
51
+
52
+ /** Rows kept per automation, so a per-minute schedule cannot grow forever. */
53
+ const RUNS_RETAINED_PER_AUTOMATION = 50;
54
+
55
+ let _initPromise: Promise<void> | undefined;
56
+
57
+ async function ensureTable(): Promise<void> {
58
+ if (!_initPromise) {
59
+ _initPromise = (async () => {
60
+ const client = getDbExec();
61
+ const createSql = `
62
+ CREATE TABLE IF NOT EXISTS ${TABLE} (
63
+ id TEXT PRIMARY KEY,
64
+ owner TEXT NOT NULL,
65
+ automation TEXT NOT NULL,
66
+ path TEXT NOT NULL,
67
+ scope TEXT,
68
+ org_id TEXT,
69
+ run_id TEXT,
70
+ thread_id TEXT,
71
+ status TEXT NOT NULL DEFAULT 'running',
72
+ started_at ${intType()} NOT NULL,
73
+ finished_at ${intType()},
74
+ error TEXT
75
+ )
76
+ `;
77
+ const indexSql = `CREATE INDEX IF NOT EXISTS idx_${TABLE}_owner_automation ON ${TABLE} (owner, automation, started_at)`;
78
+
79
+ if (isPostgres()) {
80
+ await ensureTableExists(TABLE, createSql);
81
+ await ensureIndexExists(`idx_${TABLE}_owner_automation`, indexSql);
82
+ return;
83
+ }
84
+
85
+ await client.execute(createSql);
86
+ await client.execute(indexSql);
87
+ })().catch((err) => {
88
+ _initPromise = undefined;
89
+ throw err;
90
+ });
91
+ }
92
+ return _initPromise;
93
+ }
94
+
95
+ function toRun(row: Record<string, unknown>, now: number): AutomationRun {
96
+ const stored = String(row.status) as AutomationRunStatus;
97
+ const startedAt = Number(row.started_at);
98
+ const status: AutomationRunStatus =
99
+ stored === "running" && now - startedAt > RUN_LIVENESS_CEILING_MS
100
+ ? "interrupted"
101
+ : stored;
102
+ return {
103
+ id: String(row.id),
104
+ owner: String(row.owner),
105
+ automation: String(row.automation),
106
+ path: String(row.path),
107
+ scope: row.scope == null ? null : String(row.scope),
108
+ orgId: row.org_id == null ? null : String(row.org_id),
109
+ runId: row.run_id == null ? null : String(row.run_id),
110
+ threadId: row.thread_id == null ? null : String(row.thread_id),
111
+ status,
112
+ startedAt,
113
+ finishedAt: row.finished_at == null ? null : Number(row.finished_at),
114
+ error: row.error == null ? null : String(row.error),
115
+ };
116
+ }
117
+
118
+ /**
119
+ * Record that an automation actually began executing. Returns the run id used
120
+ * to close the record out. Only real executions get a row — a tick that
121
+ * declined to run the automation must not appear in its history.
122
+ */
123
+ export async function startAutomationRun(
124
+ input: StartAutomationRunInput,
125
+ ): Promise<string> {
126
+ await ensureTable();
127
+ const id = randomUUID();
128
+ await getDbExec().execute({
129
+ sql: `INSERT INTO ${TABLE} (id, owner, automation, path, scope, org_id, run_id, thread_id, status, started_at)
130
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'running', ?)`,
131
+ args: [
132
+ id,
133
+ input.owner,
134
+ input.automation,
135
+ input.path,
136
+ input.scope ?? null,
137
+ input.orgId ?? null,
138
+ input.runId ?? null,
139
+ input.threadId ?? null,
140
+ Date.now(),
141
+ ],
142
+ });
143
+ await pruneAutomationRuns(input.owner, input.automation);
144
+ return id;
145
+ }
146
+
147
+ /**
148
+ * Drop the oldest rows for one automation once it exceeds the retention cap.
149
+ *
150
+ * Pruning on insert keeps the table bounded by the number of automations
151
+ * rather than by how often they run, and avoids a separate sweeper. The
152
+ * derived table is aliased because Postgres requires it.
153
+ */
154
+ async function pruneAutomationRuns(
155
+ owner: string,
156
+ automation: string,
157
+ ): Promise<void> {
158
+ await getDbExec().execute({
159
+ sql: `DELETE FROM ${TABLE}
160
+ WHERE owner = ? AND automation = ? AND started_at < (
161
+ SELECT MIN(started_at) FROM (
162
+ SELECT started_at FROM ${TABLE}
163
+ WHERE owner = ? AND automation = ?
164
+ ORDER BY started_at DESC
165
+ LIMIT ${RUNS_RETAINED_PER_AUTOMATION}
166
+ ) recent
167
+ )`,
168
+ args: [owner, automation, owner, automation],
169
+ });
170
+ }
171
+
172
+ export async function finishAutomationRun(
173
+ id: string,
174
+ status: Exclude<AutomationRunStatus, "running">,
175
+ error?: string,
176
+ ): Promise<void> {
177
+ await ensureTable();
178
+ await getDbExec().execute({
179
+ sql: `UPDATE ${TABLE} SET status = ?, finished_at = ?, error = ? WHERE id = ?`,
180
+ args: [status, Date.now(), error?.slice(0, MAX_ERROR_LENGTH) ?? null, id],
181
+ });
182
+ }
183
+
184
+ /**
185
+ * Attach the agent thread once it exists. The thread is created after the run
186
+ * row so the history survives a crash between the two.
187
+ */
188
+ export async function attachAutomationRunThread(
189
+ id: string,
190
+ threadId: string,
191
+ runId: string,
192
+ ): Promise<void> {
193
+ await ensureTable();
194
+ await getDbExec().execute({
195
+ sql: `UPDATE ${TABLE} SET thread_id = ?, run_id = ? WHERE id = ?`,
196
+ args: [threadId, runId, id],
197
+ });
198
+ }
199
+
200
+ /**
201
+ * Forget an automation's executions.
202
+ *
203
+ * History is keyed by the automation's name, which is reusable: deleting
204
+ * "digest" and creating a new "digest" would otherwise show the old
205
+ * definition's runs as the new one's history.
206
+ */
207
+ export async function deleteAutomationRuns(
208
+ owner: string,
209
+ automation: string,
210
+ ): Promise<void> {
211
+ await ensureTable();
212
+ // Bounded to runs that had already started. Names are reusable, so if a new
213
+ // automation takes this name and starts running before the cleanup lands,
214
+ // the cutoff keeps that run's history from being swept up with the old
215
+ // definition's.
216
+ const cutoff = Date.now();
217
+ await getDbExec().execute({
218
+ sql: `DELETE FROM ${TABLE} WHERE owner = ? AND automation = ? AND started_at <= ?`,
219
+ args: [owner, automation, cutoff],
220
+ });
221
+ }
222
+
223
+ export async function listAutomationRuns(options: {
224
+ owners: string[];
225
+ automation: string;
226
+ limit?: number;
227
+ }): Promise<AutomationRun[]> {
228
+ await ensureTable();
229
+ const owners = options.owners.filter(Boolean);
230
+ if (!owners.length) return [];
231
+ const limit = Math.min(Math.max(options.limit ?? 20, 1), 100);
232
+ const placeholders = owners.map(() => "?").join(", ");
233
+ const result = await getDbExec().execute({
234
+ sql: `SELECT * FROM ${TABLE} WHERE owner IN (${placeholders}) AND automation = ?
235
+ ORDER BY started_at DESC LIMIT ${limit}`,
236
+ args: [...owners, options.automation],
237
+ });
238
+ const now = Date.now();
239
+ return (result.rows ?? []).map((row) =>
240
+ toRun(row as Record<string, unknown>, now),
241
+ );
242
+ }
@@ -1,4 +1,5 @@
1
1
  import {
2
+ resourceGetByPath,
2
3
  resourceListAllOwners,
3
4
  resourcePut,
4
5
  type Resource,
@@ -11,7 +12,12 @@ import {
11
12
  type BackgroundAutomationContext,
12
13
  type BackgroundAutomationDeps,
13
14
  } from "./background-automation-runner.js";
14
- import { nextOccurrence, isValidCron, describeCron } from "./cron.js";
15
+ import {
16
+ nextOccurrence,
17
+ isValidCron,
18
+ describeCron,
19
+ effectiveTimezone,
20
+ } from "./cron.js";
15
21
  import {
16
22
  buildJobResourceContent,
17
23
  parseJobResource,
@@ -138,7 +144,7 @@ export async function processRecurringJobs(deps: SchedulerDeps): Promise<void> {
138
144
  // Stuck — reset so the next check can re-run it
139
145
  meta.lastStatus = "error";
140
146
  meta.lastError = "Job timed out or server crashed mid-run";
141
- const next = nextOccurrence(meta.schedule, now);
147
+ const next = nextOccurrence(meta.schedule, now, meta.timezone);
142
148
  meta.nextRun = next.toISOString();
143
149
  await updateResource(resource, meta, body);
144
150
  continue;
@@ -153,7 +159,7 @@ export async function processRecurringJobs(deps: SchedulerDeps): Promise<void> {
153
159
  // real next occurrence. Computing from new Date(0) (the epoch) always
154
160
  // returns a 1970 date, which is < now, so the job would fire
155
161
  // immediately on first sight regardless of its schedule.
156
- const next = nextOccurrence(meta.schedule, now);
162
+ const next = nextOccurrence(meta.schedule, now, meta.timezone);
157
163
  meta.nextRun = next.toISOString();
158
164
  await updateResource(resource, meta, body);
159
165
  continue;
@@ -216,10 +222,16 @@ async function executeJob(
216
222
  `User/membership no longer valid — leaving cron entry for admin review.`,
217
223
  );
218
224
  // Mark as skipped without resetting nextRun so an admin can find it.
219
- meta.lastRun = now.toISOString();
225
+ // `lastRun` is deliberately untouched: the job did not run, and stamping
226
+ // it here made a permanently blocked job look like it ran every minute.
227
+ // Re-writing an unchanged resource on every tick also churns the poll
228
+ // stream, so only persist when the failure state actually changed.
229
+ const alreadyRecorded =
230
+ meta.lastStatus === "skipped" && meta.lastError === identity.reason;
231
+ meta.lastCheck = now.toISOString();
220
232
  meta.lastStatus = "skipped";
221
233
  meta.lastError = identity.reason;
222
- await updateResource(resource, meta, body);
234
+ if (!alreadyRecorded) await updateResource(resource, meta, body);
223
235
  return;
224
236
  }
225
237
  const jobUserEmail = identity.identity.userEmail;
@@ -263,7 +275,7 @@ async function executeJob(
263
275
  automation: jobContext,
264
276
  ownerEmail: jobUserEmail,
265
277
  orgId: jobOrgId,
266
- prompt: `[Recurring Job: ${jobName}]\nSchedule: ${describeCron(meta.schedule)}\n\nExecute the following job instructions:\n\n${body}`,
278
+ prompt: `[Recurring Job: ${jobName}]\nSchedule: ${describeCron(meta.schedule, effectiveTimezone(meta.timezone))}\n\nExecute the following job instructions:\n\n${body}`,
267
279
  threadTitle: `Job: ${jobName} — ${now.toLocaleDateString()}`,
268
280
  runIdPrefix: `job-${jobName}`,
269
281
  usageLabel: `recurring-job:${jobName}`,
@@ -272,20 +284,21 @@ async function executeJob(
272
284
  deps,
273
285
  );
274
286
 
275
- // Compute from completion time so long runs cannot immediately re-fire.
276
- meta.lastStatus = "success";
277
- meta.nextRun = nextOccurrence(meta.schedule, new Date()).toISOString();
278
- await updateResource(resource, meta, body);
279
- console.log(
280
- `[recurring-jobs] Job "${jobName}" completed. Next run: ${meta.nextRun}`,
281
- );
287
+ await recordExecutionOutcome(resource, {
288
+ lastRun: meta.lastRun,
289
+ lastStatus: "success",
290
+ lastError: undefined,
291
+ });
292
+ console.log(`[recurring-jobs] Job "${jobName}" completed.`);
282
293
  } catch (err) {
283
- meta.lastStatus = "error";
284
- meta.lastError =
294
+ const lastError =
285
295
  err instanceof Error ? err.message.slice(0, 200) : "Unknown error";
286
- meta.nextRun = nextOccurrence(meta.schedule, new Date()).toISOString();
287
- await updateResource(resource, meta, body);
288
- console.error(`[recurring-jobs] Job "${jobName}" failed:`, meta.lastError);
296
+ await recordExecutionOutcome(resource, {
297
+ lastRun: meta.lastRun,
298
+ lastStatus: "error",
299
+ lastError,
300
+ });
301
+ console.error(`[recurring-jobs] Job "${jobName}" failed:`, lastError);
289
302
  }
290
303
  }
291
304
 
@@ -297,3 +310,45 @@ async function updateResource(
297
310
  const content = buildJobContent(meta, body);
298
311
  await resourcePut(resource.owner, resource.path, content);
299
312
  }
313
+
314
+ /** Execution bookkeeping the scheduler owns; the rest belongs to the editor. */
315
+ type ExecutionOutcome = Pick<
316
+ JobFrontmatter,
317
+ "lastRun" | "lastCheck" | "lastStatus" | "lastError"
318
+ >;
319
+
320
+ /**
321
+ * Persist the result of a run without clobbering a concurrent edit.
322
+ *
323
+ * A run holds its `meta` for as long as the job takes, so writing that whole
324
+ * snapshot back on completion would silently revert a schedule, timezone or
325
+ * instruction change made while it was running. Only the execution fields are
326
+ * ours to write, and `nextRun` is recomputed from whatever schedule is stored
327
+ * now, so an edit mid-run takes effect on the next tick.
328
+ */
329
+ async function recordExecutionOutcome(
330
+ resource: Resource,
331
+ outcome: ExecutionOutcome,
332
+ ): Promise<void> {
333
+ const latest = await resourceGetByPath(resource.owner, resource.path);
334
+ if (!latest) {
335
+ // Deleted while it was running. Writing the pre-run snapshot back would
336
+ // resurrect the automation and schedule it again.
337
+ console.log(
338
+ `[recurring-jobs] "${resource.path}" was deleted mid-run; dropping its outcome.`,
339
+ );
340
+ return;
341
+ }
342
+ const current = parseJobResource(latest.content);
343
+
344
+ const meta: JobFrontmatter = { ...current.meta, ...outcome };
345
+ if (meta.schedule && isValidCron(meta.schedule)) {
346
+ // Measured from completion so a long run cannot immediately re-fire.
347
+ meta.nextRun = nextOccurrence(
348
+ meta.schedule,
349
+ new Date(),
350
+ meta.timezone,
351
+ ).toISOString();
352
+ }
353
+ await updateResource(resource, meta, current.body);
354
+ }
@@ -1,5 +1,6 @@
1
1
  import type { ActionEntry } from "../agent/production-agent.js";
2
2
  import { getDbExec } from "../db/client.js";
3
+ import { resolveUserSchedulingTimezone } from "../localization/user-timezone.js";
3
4
  import {
4
5
  resourcePut,
5
6
  resourceGetByPath,
@@ -14,7 +15,13 @@ import {
14
15
  getRequestOrgId,
15
16
  getIntegrationRequestContext,
16
17
  } from "../server/request-context.js";
17
- import { isValidCron, nextOccurrence, describeCron } from "./cron.js";
18
+ import {
19
+ isValidCron,
20
+ nextOccurrence,
21
+ describeCron,
22
+ effectiveTimezone,
23
+ isValidTimezone,
24
+ } from "./cron.js";
18
25
  import { classifyJobResource } from "./frontmatter.js";
19
26
  import {
20
27
  parseJobFrontmatter,
@@ -95,6 +102,7 @@ export async function authorizeJobMutation(
95
102
 
96
103
  async function runCreate(args: Record<string, any>): Promise<string> {
97
104
  const { name, schedule, instructions, scope, runAs, model } = args;
105
+ const requestedTimezone = args.timezone;
98
106
 
99
107
  if (!name || !schedule || !instructions) {
100
108
  return JSON.stringify({
@@ -118,13 +126,24 @@ async function runCreate(args: Record<string, any>): Promise<string> {
118
126
  const owner = scope === "personal" ? getOwner() : getSharedOwner();
119
127
  const path = `jobs/${name}.md`;
120
128
  const now = new Date();
121
- const next = nextOccurrence(schedule, now);
129
+ // A cron time with no zone silently means the host's zone, which is how an
130
+ // "8am" job ends up firing at 4am for the person who asked for it.
131
+ if (requestedTimezone && !isValidTimezone(requestedTimezone)) {
132
+ return JSON.stringify({
133
+ error: `Unknown timezone: "${requestedTimezone}". Use an IANA zone such as America/New_York.`,
134
+ });
135
+ }
136
+ const timezone =
137
+ requestedTimezone ||
138
+ (await resolveUserSchedulingTimezone(getRequestUserEmail()));
139
+ const next = nextOccurrence(schedule, now, timezone);
122
140
  const integration = getIntegrationRequestContext();
123
141
  const channelId = integration?.incoming.platformContext.channelId;
124
142
  const threadRef = integration?.incoming.threadRef;
125
143
 
126
144
  const meta: JobFrontmatter = {
127
145
  schedule,
146
+ timezone,
128
147
  enabled: true,
129
148
  createdBy: getOwner(),
130
149
  orgId: getRequestOrgId() || undefined,
@@ -155,7 +174,8 @@ async function runCreate(args: Record<string, any>): Promise<string> {
155
174
  name,
156
175
  path,
157
176
  schedule,
158
- scheduleDescription: describeCron(schedule),
177
+ timezone,
178
+ scheduleDescription: describeCron(schedule, timezone),
159
179
  nextRun: next.toISOString(),
160
180
  scope: scope || "shared",
161
181
  ...(mcpTools?.length ? { mcpTools } : {}),
@@ -187,7 +207,10 @@ async function runList(args: Record<string, any>): Promise<string> {
187
207
  path: r.path,
188
208
  scope: r.owner === sharedOwner ? "shared" : "personal",
189
209
  schedule: meta.schedule,
190
- scheduleDescription: meta.schedule ? describeCron(meta.schedule) : "",
210
+ timezone: effectiveTimezone(meta.timezone),
211
+ scheduleDescription: meta.schedule
212
+ ? describeCron(meta.schedule, effectiveTimezone(meta.timezone))
213
+ : "",
191
214
  enabled: meta.enabled,
192
215
  lastRun: meta.lastRun || null,
193
216
  lastStatus: meta.lastStatus || null,
@@ -248,7 +271,23 @@ async function runUpdate(args: Record<string, any>): Promise<string> {
248
271
  });
249
272
  }
250
273
  meta.schedule = schedule;
251
- meta.nextRun = nextOccurrence(schedule).toISOString();
274
+ }
275
+
276
+ if (args.timezone !== undefined) {
277
+ if (!isValidTimezone(args.timezone)) {
278
+ return JSON.stringify({
279
+ error: `Unknown timezone: "${args.timezone}".`,
280
+ });
281
+ }
282
+ meta.timezone = args.timezone;
283
+ }
284
+
285
+ if (schedule || args.timezone !== undefined) {
286
+ meta.nextRun = nextOccurrence(
287
+ meta.schedule,
288
+ undefined,
289
+ meta.timezone,
290
+ ).toISOString();
252
291
  }
253
292
 
254
293
  if (enabled !== undefined) {
@@ -281,7 +320,11 @@ async function runUpdate(args: Record<string, any>): Promise<string> {
281
320
  updated: true,
282
321
  name,
283
322
  schedule: meta.schedule,
284
- scheduleDescription: describeCron(meta.schedule),
323
+ timezone: effectiveTimezone(meta.timezone),
324
+ scheduleDescription: describeCron(
325
+ meta.schedule,
326
+ effectiveTimezone(meta.timezone),
327
+ ),
285
328
  enabled: meta.enabled,
286
329
  nextRun: meta.nextRun,
287
330
  mcpTools: meta.mcpTools || [],
@@ -347,6 +390,11 @@ For jobs that use a connected MCP, pass the exact tool names in mcpTools. This b
347
390
  description:
348
391
  "Job name (hyphen-case, e.g. 'daily-scorecard-check'). Required for create and update.",
349
392
  },
393
+ timezone: {
394
+ type: "string",
395
+ description:
396
+ "IANA timezone the schedule's clock time is read in, e.g. 'America/New_York'. Optional; defaults to the user's saved scheduling timezone, then the caller's browser zone. Always pass this when the user names a time of day, so '8am' means 8am where they are rather than on the server.",
397
+ },
350
398
  schedule: {
351
399
  type: "string",
352
400
  description:
@@ -5,7 +5,7 @@ import { getUserSetting } from "../../settings/user-settings.js";
5
5
  import {
6
6
  LOCALIZATION_SETTING_KEY,
7
7
  normalizeLocalizationPreference,
8
- type LocalizationPreference,
8
+ type ResolvedLocalizationPreference,
9
9
  } from "../shared.js";
10
10
 
11
11
  export default defineAction({
@@ -13,7 +13,7 @@ export default defineAction({
13
13
  "Get the current user's interface language preference. Returns { locale }, where locale is 'system' or a supported BCP-47 locale.",
14
14
  schema: z.object({}),
15
15
  http: { method: "GET" },
16
- run: async (_args, ctx): Promise<LocalizationPreference> => {
16
+ run: async (_args, ctx): Promise<ResolvedLocalizationPreference> => {
17
17
  if (!ctx?.userEmail) throw new Error("Not authenticated.");
18
18
  const stored = await getUserSetting(
19
19
  ctx.userEmail,
@@ -1,31 +1,69 @@
1
1
  import { z } from "zod";
2
2
 
3
3
  import { defineAction } from "../../action.js";
4
- import { putUserSetting } from "../../settings/user-settings.js";
4
+ import {
5
+ getUserSetting,
6
+ putUserSetting,
7
+ } from "../../settings/user-settings.js";
5
8
  import {
6
9
  LOCALIZATION_SETTING_KEY,
7
10
  SUPPORTED_LOCALES,
8
11
  normalizeLocalePreference,
9
- type LocalizationPreference,
12
+ normalizeLocalizationPreference,
13
+ normalizeTimezonePreference,
14
+ type ResolvedLocalizationPreference,
10
15
  } from "../shared.js";
11
16
 
12
17
  export default defineAction({
13
18
  description:
14
- "Set the current user's interface language preference to 'system' or a supported BCP-47 locale.",
19
+ "Set the current user's interface language and scheduling timezone. Locale is 'system' or a supported BCP-47 locale; timezone is 'system' or an IANA zone such as America/New_York.",
15
20
  schema: z.object({
16
21
  locale: z
17
22
  .string()
18
- .describe("Language preference: 'system' or a supported BCP-47 locale."),
23
+ .describe("Language preference: 'system' or a supported BCP-47 locale.")
24
+ .optional(),
25
+ timezone: z
26
+ .string()
27
+ .describe("Scheduling timezone: 'system' or an IANA zone name.")
28
+ .optional(),
19
29
  }),
20
- run: async (args, ctx): Promise<LocalizationPreference> => {
30
+ run: async (args, ctx): Promise<ResolvedLocalizationPreference> => {
21
31
  if (!ctx?.userEmail) throw new Error("Not authenticated.");
22
- const locale = normalizeLocalePreference(args.locale);
23
- if (!locale) {
24
- throw new Error(
25
- `Unsupported locale. Use system, ${SUPPORTED_LOCALES.join(", ")}.`,
26
- );
32
+
33
+ // Each field is optional, so merge onto what is stored instead of
34
+ // writing a partial record that silently resets the other one.
35
+ const current = normalizeLocalizationPreference(
36
+ await getUserSetting(ctx.userEmail, LOCALIZATION_SETTING_KEY),
37
+ );
38
+
39
+ let locale = current.locale;
40
+ if (args.locale !== undefined) {
41
+ const parsed = normalizeLocalePreference(args.locale);
42
+ if (!parsed) {
43
+ throw new Error(
44
+ `Unsupported locale. Use system, ${SUPPORTED_LOCALES.join(", ")}.`,
45
+ );
46
+ }
47
+ locale = parsed;
27
48
  }
28
- const value: LocalizationPreference & Record<string, unknown> = { locale };
49
+
50
+ let timezone = current.timezone;
51
+ if (args.timezone !== undefined) {
52
+ const parsed = normalizeTimezonePreference(args.timezone);
53
+ // Normalizing an unusable zone to "system" would quietly schedule the
54
+ // user in the host zone, which is the failure this field exists to fix.
55
+ if (parsed === "system" && args.timezone.trim() !== "system") {
56
+ throw new Error(
57
+ `Unknown timezone "${args.timezone}". Use system or an IANA zone such as America/New_York.`,
58
+ );
59
+ }
60
+ timezone = parsed;
61
+ }
62
+
63
+ const value: ResolvedLocalizationPreference & Record<string, unknown> = {
64
+ locale,
65
+ timezone,
66
+ };
29
67
  await putUserSetting(ctx.userEmail, LOCALIZATION_SETTING_KEY, value);
30
68
  return value;
31
69
  },
@@ -57,6 +57,10 @@ const messages = {
57
57
  profileSaved: "Profile updated",
58
58
  profileSaveError: "Could not update profile",
59
59
  profileMenuItem: "Profile",
60
+ manageAgentMenuItem: "Manage agent",
61
+ timezoneLabel: "Timezone",
62
+ timezoneSystem: "Follow this browser ({{zone}})",
63
+ timezoneHint: "Used for timestamps and scheduled automations.",
60
64
  },
61
65
  secrets: {
62
66
  scopeLabel: "Scope",
@@ -563,6 +567,7 @@ const messages = {
563
567
  systemOrdered: "System · ordered, not evictable",
564
568
  },
565
569
  jobs: {
570
+ timezone: "Timezone",
566
571
  pageTitle: "Automations",
567
572
  pageDescription:
568
573
  "Manage agent tasks that run on a schedule or in response to events.",
@@ -582,6 +587,28 @@ const messages = {
582
587
  paused: "Paused",
583
588
  nextRun: "Next run",
584
589
  lastRun: "Last run",
590
+ lastChecked: "Last checked",
591
+ lastStatus: "Last status",
592
+ neverRan: "Never",
593
+ notSet: "—",
594
+ status: "Status",
595
+ trigger: "Trigger",
596
+ scope: "Scope",
597
+ createdBy: "Created by",
598
+ model: "Model",
599
+ blockedTitle: "This automation is not running",
600
+ pastRuns: "Past runs",
601
+ runsLoadError: "Could not load run history.",
602
+ noRuns:
603
+ "This automation has not executed yet. Only real executions are recorded here.",
604
+ edit: "Edit",
605
+ editScheduleTitle: "Edit schedule — {{name}}",
606
+ editScheduleDescription:
607
+ "The clock time below is read in the timezone you pick, so 8:00 means 8:00 there.",
608
+ cronExpression: "Cron expression",
609
+ cronFormatHint: "minute hour day-of-month month day-of-week",
610
+ cronFieldCount: "A cron expression needs exactly 5 fields.",
611
+ saveSchedule: "Save schedule",
585
612
  details: "Details",
586
613
  pause: "Pause",
587
614
  resume: "Resume",