@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
@@ -32,6 +32,11 @@ import {
32
32
  type RequestContext,
33
33
  } from "../server/request-context.js";
34
34
  import type { JobFrontmatter } from "./frontmatter.js";
35
+ import {
36
+ attachAutomationRunThread,
37
+ finishAutomationRun,
38
+ startAutomationRun,
39
+ } from "./run-history.js";
35
40
 
36
41
  const BACKGROUND_RUN_STUCK_MS = 10 * 60_000;
37
42
  const BACKGROUND_RUN_HARD_TIMEOUT_MS = 5 * 60_000;
@@ -248,6 +253,87 @@ function createRunId(prefix: string): string {
248
253
  export async function runBackgroundAutomation(
249
254
  options: BackgroundAutomationRunOptions,
250
255
  deps: BackgroundAutomationDeps,
256
+ ): Promise<BackgroundAutomationRunResult> {
257
+ const { automation } = options;
258
+ // Bookkeeping, so it must not gate the work it describes: a history table
259
+ // that cannot be written should cost us the record, not the automation.
260
+ // Everything downstream tolerates a null id by skipping its own write.
261
+ let historyId: string | null = null;
262
+ try {
263
+ historyId = await startAutomationRun({
264
+ owner: automation.resource.owner,
265
+ automation: automation.name,
266
+ path: automation.resource.path,
267
+ scope: organizationIdFromResourceOwner(automation.resource.owner)
268
+ ? "organization"
269
+ : "personal",
270
+ orgId: options.orgId ?? null,
271
+ });
272
+ } catch (err) {
273
+ console.error(
274
+ `[automations] Could not open a history record for "${automation.name}"; running anyway:`,
275
+ err,
276
+ );
277
+ }
278
+
279
+ let result: BackgroundAutomationRunResult;
280
+ try {
281
+ result = await executeBackgroundAutomation(options, deps, historyId);
282
+ } catch (err) {
283
+ await recordRunOutcome(
284
+ historyId,
285
+ "error",
286
+ err instanceof Error ? err.message : String(err),
287
+ );
288
+ throw err;
289
+ }
290
+ // Outside the try: history is bookkeeping about the run, so a failure to
291
+ // write it must not turn a completed automation into a reported failure.
292
+ await recordRunOutcome(historyId, "success");
293
+ return result;
294
+ }
295
+
296
+ /**
297
+ * Link the run to its agent thread. Bookkeeping again: the automation is
298
+ * already executing by this point, so a failed write costs the cross-reference
299
+ * in the history view, not the run.
300
+ */
301
+ async function recordRunThread(
302
+ historyId: string | null,
303
+ threadId: string,
304
+ runId: string,
305
+ ): Promise<void> {
306
+ if (!historyId) return;
307
+ try {
308
+ await attachAutomationRunThread(historyId, threadId, runId);
309
+ } catch (err) {
310
+ console.error(
311
+ `[automations] Could not attach thread ${threadId} to run ${historyId}:`,
312
+ err,
313
+ );
314
+ }
315
+ }
316
+
317
+ async function recordRunOutcome(
318
+ historyId: string | null,
319
+ status: "success" | "error",
320
+ error?: string,
321
+ ): Promise<void> {
322
+ if (!historyId) return;
323
+ try {
324
+ await finishAutomationRun(historyId, status, error);
325
+ } catch (err) {
326
+ console.error(
327
+ `[automations] Could not record run ${historyId} as ${status}:`,
328
+ err,
329
+ );
330
+ }
331
+ }
332
+
333
+ async function executeBackgroundAutomation(
334
+ options: BackgroundAutomationRunOptions,
335
+ deps: BackgroundAutomationDeps,
336
+ historyId: string | null,
251
337
  ): Promise<BackgroundAutomationRunResult> {
252
338
  const { automation, ownerEmail, orgId, prompt, threadTitle, usageLabel } =
253
339
  options;
@@ -291,6 +377,8 @@ export async function runBackgroundAutomation(
291
377
  const systemPrompt = await deps.getSystemPrompt(ownerEmail);
292
378
  const thread = await createThread(ownerEmail, { title: threadTitle });
293
379
  const runId = createRunId(options.runIdPrefix);
380
+ await recordRunThread(historyId, thread.id, runId);
381
+
294
382
  // Scheduled work is background work: it has no synchronous serverless
295
383
  // caller waiting on it, so it must not inherit the interactive clamp
296
384
  // (40s soft timeout, a 30s no-progress backstop at 0.75x that, and 6
package/src/jobs/cron.ts CHANGED
@@ -9,12 +9,50 @@ function normalize(cronExpr: string): string {
9
9
  return ALIAS_MAP[cronExpr.trim().toLowerCase()] ?? cronExpr;
10
10
  }
11
11
 
12
+ /**
13
+ * IANA zone the host process evaluates wall-clock times in. Jobs stored before
14
+ * schedules carried a `timezone` are interpreted here, so their meaning does
15
+ * not silently shift when timezone support is added.
16
+ */
17
+ export function serverTimezone(): string {
18
+ try {
19
+ return Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
20
+ } catch {
21
+ return "UTC";
22
+ }
23
+ }
24
+
25
+ export function isValidTimezone(timezone: string): boolean {
26
+ if (!timezone.trim()) return false;
27
+ try {
28
+ new Intl.DateTimeFormat("en-US", { timeZone: timezone });
29
+ return true;
30
+ } catch {
31
+ return false;
32
+ }
33
+ }
34
+
35
+ /**
36
+ * Resolve the zone a schedule's wall-clock fields are read in. An absent
37
+ * `timezone` means the schedule predates timezone support, so it keeps its
38
+ * original host-relative meaning rather than being reinterpreted as UTC.
39
+ */
40
+ export function effectiveTimezone(timezone?: string | null): string {
41
+ return timezone && isValidTimezone(timezone) ? timezone : serverTimezone();
42
+ }
43
+
12
44
  /**
13
45
  * Compute the next occurrence of a cron expression after the given date.
46
+ * `timezone` is the IANA zone the expression's wall-clock fields refer to.
14
47
  */
15
- export function nextOccurrence(cronExpr: string, after?: Date): Date {
48
+ export function nextOccurrence(
49
+ cronExpr: string,
50
+ after?: Date,
51
+ timezone?: string | null,
52
+ ): Date {
16
53
  const expr = CronExpressionParser.parse(normalize(cronExpr), {
17
54
  currentDate: after ?? new Date(),
55
+ tz: effectiveTimezone(timezone),
18
56
  });
19
57
  const next = expr.next();
20
58
  return next.toDate();
@@ -36,13 +74,21 @@ export function isValidCron(cronExpr: string): boolean {
36
74
  * Convert a cron expression to a human-readable description.
37
75
  * Handles common patterns; falls back to the raw expression for unusual ones.
38
76
  */
39
- export function describeCron(cronExpr: string): string {
77
+ export function describeCron(
78
+ cronExpr: string,
79
+ timezone?: string | null,
80
+ ): string {
40
81
  const normalized = normalize(cronExpr);
41
82
  const parts = normalized.trim().split(/\s+/);
42
83
  if (parts.length !== 5) return cronExpr;
43
84
 
44
85
  const [minute, hour, dayOfMonth, month, dayOfWeek] = parts;
45
86
 
87
+ // A bare wall-clock time is ambiguous the moment the host and the reader sit
88
+ // in different zones, so every description carrying a time names its zone.
89
+ const zone = (text: string) =>
90
+ timezone ? text + " (" + timezone + ")" : text;
91
+
46
92
  // Every minute
47
93
  if (normalized === "* * * * *") return "Every minute";
48
94
 
@@ -98,7 +144,7 @@ export function describeCron(cronExpr: string): string {
98
144
 
99
145
  // Every day at specific time
100
146
  if (dayOfMonth === "*" && month === "*" && dayOfWeek === "*" && time) {
101
- return `Every day at ${time}`;
147
+ return zone(`Every day at ${time}`);
102
148
  }
103
149
 
104
150
  // Weekdays
@@ -108,18 +154,18 @@ export function describeCron(cronExpr: string): string {
108
154
  (dayOfWeek === "1-5" || dayOfWeek === "MON-FRI") &&
109
155
  time
110
156
  ) {
111
- return `Every weekday at ${time}`;
157
+ return zone(`Every weekday at ${time}`);
112
158
  }
113
159
 
114
160
  // Specific day of week
115
161
  if (dayOfMonth === "*" && month === "*" && dayOfWeek !== "*" && time) {
116
162
  const days = dayOfWeek.split(",").map((d) => dayNames[d] || d);
117
- return `Every ${days.join(", ")} at ${time}`;
163
+ return zone(`Every ${days.join(", ")} at ${time}`);
118
164
  }
119
165
 
120
166
  // Specific day of month
121
167
  if (dayOfMonth !== "*" && month === "*" && dayOfWeek === "*" && time) {
122
- return `On day ${dayOfMonth} of every month at ${time}`;
168
+ return zone(`On day ${dayOfMonth} of every month at ${time}`);
123
169
  }
124
170
 
125
171
  return cronExpr;
@@ -11,10 +11,22 @@ export type JobExecutionMode = "agentic" | "deterministic";
11
11
  export interface JobFrontmatter {
12
12
  schedule: string;
13
13
  enabled: boolean;
14
+ /**
15
+ * IANA zone the cron fields are read in. Absent means the schedule predates
16
+ * timezone support and keeps its original host-relative meaning.
17
+ */
18
+ timezone?: string;
14
19
  createdBy?: string;
15
20
  orgId?: string;
16
21
  runAs?: "creator" | "shared";
22
+ /** Last time the automation actually started executing. */
17
23
  lastRun?: string;
24
+ /**
25
+ * Last time a tick evaluated this automation and declined to run it. Kept
26
+ * distinct from `lastRun` so a blocked automation cannot report a run it
27
+ * never performed.
28
+ */
29
+ lastCheck?: string;
18
30
  lastStatus?: JobLastStatus;
19
31
  lastError?: string;
20
32
  nextRun?: string;
@@ -132,6 +144,9 @@ function parseKnownField(
132
144
  case "enabled":
133
145
  meta.enabled = value !== "false";
134
146
  break;
147
+ case "timezone":
148
+ meta.timezone = value || undefined;
149
+ break;
135
150
  case "createdBy":
136
151
  meta.createdBy = value;
137
152
  break;
@@ -145,6 +160,9 @@ function parseKnownField(
145
160
  case "lastRun":
146
161
  meta.lastRun = value;
147
162
  break;
163
+ case "lastCheck":
164
+ meta.lastCheck = value;
165
+ break;
148
166
  case "lastStatus":
149
167
  meta.lastStatus = value as JobLastStatus;
150
168
  break;
@@ -290,7 +308,9 @@ export function buildJobResourceContent(
290
308
  pushString(lines, "createdBy", meta.createdBy, false);
291
309
  pushString(lines, "orgId", meta.orgId);
292
310
  if (meta.runAs) lines.push(`runAs: ${meta.runAs}`);
311
+ pushString(lines, "timezone", meta.timezone);
293
312
  pushString(lines, "lastRun", meta.lastRun);
313
+ pushString(lines, "lastCheck", meta.lastCheck);
294
314
  if (meta.lastStatus) lines.push(`lastStatus: ${meta.lastStatus}`);
295
315
  pushString(lines, "lastError", meta.lastError);
296
316
  pushString(lines, "nextRun", meta.nextRun);
@@ -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
+ }