@agent-native/core 0.134.0 → 0.135.0

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 (493) hide show
  1. package/corpus/README.md +3 -3
  2. package/corpus/core/CHANGELOG.md +113 -0
  3. package/corpus/core/docs/content/template-content-developers.mdx +1 -1
  4. package/corpus/core/package.json +3 -2
  5. package/corpus/core/src/automations/service.ts +41 -4
  6. package/corpus/core/src/cli/mcp-config-writers.ts +27 -21
  7. package/corpus/core/src/client/agent-page/AgentJobsTab.tsx +187 -85
  8. package/corpus/core/src/client/agent-page/AutomationDetailsDialog.tsx +208 -0
  9. package/corpus/core/src/client/agent-page/AutomationScheduleDialog.tsx +180 -0
  10. package/corpus/core/src/client/agent-page/TimezoneSelect.tsx +241 -0
  11. package/corpus/core/src/client/agent-page/use-jobs.ts +50 -10
  12. package/corpus/core/src/client/org/OrgSwitcher.tsx +26 -0
  13. package/corpus/core/src/client/settings/AccountSettingsCard.tsx +35 -31
  14. package/corpus/core/src/client/settings/SchedulingTimezoneField.tsx +71 -0
  15. package/corpus/core/src/client/settings/SettingsRow.tsx +113 -0
  16. package/corpus/core/src/client/settings/agent-settings-search.ts +2 -1
  17. package/corpus/core/src/client/settings/index.ts +6 -0
  18. package/corpus/core/src/integrations/identity.ts +2 -12
  19. package/corpus/core/src/jobs/actions/list-automation-runs.ts +33 -0
  20. package/corpus/core/src/jobs/actions/list-recurring-jobs.ts +27 -6
  21. package/corpus/core/src/jobs/actions/manage-recurring-job.ts +41 -9
  22. package/corpus/core/src/jobs/background-automation-runner.ts +88 -0
  23. package/corpus/core/src/jobs/cron.ts +52 -6
  24. package/corpus/core/src/jobs/frontmatter.ts +20 -0
  25. package/corpus/core/src/jobs/run-history.ts +242 -0
  26. package/corpus/core/src/jobs/scheduler.ts +73 -18
  27. package/corpus/core/src/jobs/tools.ts +54 -6
  28. package/corpus/core/src/localization/actions/get-localization-preference.ts +2 -2
  29. package/corpus/core/src/localization/actions/set-localization-preference.ts +49 -11
  30. package/corpus/core/src/localization/default-messages.ts +27 -0
  31. package/corpus/core/src/localization/shared.ts +39 -6
  32. package/corpus/core/src/localization/user-timezone.ts +36 -0
  33. package/corpus/core/src/org/app-roles-handlers.ts +1 -9
  34. package/corpus/core/src/org/index.ts +2 -0
  35. package/corpus/core/src/org/membership.ts +18 -0
  36. package/corpus/core/src/review/actions/create-review-comment.ts +4 -1
  37. package/corpus/core/src/review/actions/reply-review-comment.ts +4 -1
  38. package/corpus/core/src/review/index.ts +5 -0
  39. package/corpus/core/src/review/notifications.ts +163 -0
  40. package/corpus/core/src/review/types.ts +7 -0
  41. package/corpus/core/src/server/action-discovery.ts +4 -0
  42. package/corpus/core/src/server/activity-notifications.ts +174 -0
  43. package/corpus/core/src/server/app-name.ts +31 -0
  44. package/corpus/core/src/server/better-auth-instance.ts +4 -4
  45. package/corpus/core/src/server/email-templates.ts +70 -14
  46. package/corpus/core/src/server/email.ts +66 -3
  47. package/corpus/core/src/server/index.ts +10 -0
  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-29-meeting-notes-no-longer-transcribe-the-other-side-twice-when.md +6 -0
  109. package/corpus/templates/clips/changelog/2026-07-31-editor-timeline-now-shows-video-frames-behind-the-waveform-g.md +6 -0
  110. package/corpus/templates/clips/changelog/2026-07-31-email-notifications-now-go-out-when-someone-comments-on-or-r.md +6 -0
  111. package/corpus/templates/clips/changelog/2026-07-31-you-can-now-set-a-default-visibility-for-new-recordings-in-s.md +6 -0
  112. package/corpus/templates/clips/changelog/2026-08-01-email-notifications-for-comments-and-reactions-now-actually-.md +6 -0
  113. package/corpus/templates/clips/changelog/2026-08-03-the-desktop-app-now-opens-directly-beneath-its-tray-icon-on-.md +6 -0
  114. package/corpus/templates/clips/chrome-extension/package.json +1 -0
  115. package/corpus/templates/clips/chrome-extension/vitest.config.ts +9 -0
  116. package/corpus/templates/clips/desktop/package.json +1 -0
  117. package/corpus/templates/clips/desktop/src/app.tsx +17 -1
  118. package/corpus/templates/clips/desktop/src/hooks/useMeetingTranscription.ts +37 -48
  119. package/corpus/templates/clips/desktop/src/lib/recorder.ts +331 -36
  120. package/corpus/templates/clips/desktop/src/lib/transcription-capture.ts +7 -5
  121. package/corpus/templates/clips/desktop/src/lib/transcription-engine.ts +182 -106
  122. package/corpus/templates/clips/desktop/src/lib/upload-recovery.ts +129 -0
  123. package/corpus/templates/clips/desktop/src/overlays/live-transcript.tsx +43 -14
  124. package/corpus/templates/clips/desktop/src/overlays/recording-pill.tsx +3 -3
  125. package/corpus/templates/clips/desktop/src-tauri/src/echo_guard.rs +443 -0
  126. package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +1 -0
  127. package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +815 -15
  128. package/corpus/templates/clips/desktop/src-tauri/src/tray.rs +61 -11
  129. package/corpus/templates/clips/desktop/src-tauri/src/whisper_speech.rs +125 -16
  130. package/corpus/templates/clips/desktop/vitest.config.ts +9 -0
  131. package/corpus/templates/clips/server/db/schema.ts +17 -0
  132. package/corpus/templates/clips/server/jobs/media-verification.ts +4 -0
  133. package/corpus/templates/clips/server/lib/activity-notifications.ts +185 -0
  134. package/corpus/templates/clips/server/lib/recording-media-cleanup.ts +2 -0
  135. package/corpus/templates/clips/server/lib/recording-upload-state.ts +46 -9
  136. package/corpus/templates/clips/server/lib/recordings.ts +24 -0
  137. package/corpus/templates/clips/server/lib/resumable-session.ts +10 -4
  138. package/corpus/templates/clips/server/lib/transactional-email-templates.ts +109 -0
  139. package/corpus/templates/clips/server/lib/upload-interruption.ts +8 -0
  140. package/corpus/templates/clips/server/lib/upload-lease.ts +50 -4
  141. package/corpus/templates/clips/server/lib/video-filmstrip-sprite.ts +207 -0
  142. package/corpus/templates/clips/server/lib/video-remux.ts +13 -4
  143. package/corpus/templates/clips/server/plugins/db.ts +16 -0
  144. package/corpus/templates/clips/server/routes/api/_agent-native-background/post-finalize-worker.post.ts +4 -0
  145. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/abort.post.ts +123 -36
  146. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/chunk.post.ts +159 -29
  147. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/interrupt.post.ts +197 -0
  148. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/reset-chunks.post.ts +193 -39
  149. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/resume.get.ts +191 -12
  150. package/corpus/templates/clips/shared/clips-ai-prefs.ts +6 -0
  151. package/corpus/templates/clips/shared/feature-flags.ts +12 -0
  152. package/corpus/templates/clips/shared/recording-core.ts +4 -0
  153. package/corpus/templates/clips/vitest.config.ts +23 -19
  154. package/corpus/templates/content/.agents/skills/document-editing/references/databases.md +6 -4
  155. package/corpus/templates/content/actions/_database-membership-lock.ts +25 -0
  156. package/corpus/templates/content/actions/_database-row-batch.ts +6 -2
  157. package/corpus/templates/content/actions/_database-source-utils.ts +289 -129
  158. package/corpus/templates/content/actions/_database-utils.ts +12 -3
  159. package/corpus/templates/content/actions/add-comment.ts +15 -1
  160. package/corpus/templates/content/actions/add-content-database-source-field-property.ts +39 -21
  161. package/corpus/templates/content/actions/attach-content-database-source.ts +4 -1
  162. package/corpus/templates/content/actions/change-content-database-source-role.ts +3 -5
  163. package/corpus/templates/content/actions/duplicate-document-property.ts +59 -46
  164. package/corpus/templates/content/actions/execute-builder-source-execution.ts +77 -71
  165. package/corpus/templates/content/actions/list-documents.ts +1 -0
  166. package/corpus/templates/content/actions/materialize-builder-required-fields.ts +7 -0
  167. package/corpus/templates/content/actions/remove-database-items.ts +183 -0
  168. package/corpus/templates/content/actions/set-document-property.ts +57 -39
  169. package/corpus/templates/content/actions/sync-local-folder-source.ts +7 -0
  170. package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +5 -2
  171. package/corpus/templates/content/app/components/editor/DocumentToolbar.tsx +50 -1
  172. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +471 -329
  173. package/corpus/templates/content/app/components/editor/database/row-access.ts +45 -0
  174. package/corpus/templates/content/app/components/editor/database/shared.tsx +141 -38
  175. package/corpus/templates/content/app/components/ui/switch.tsx +1 -0
  176. package/corpus/templates/content/app/hooks/use-content-database.ts +2 -2
  177. package/corpus/templates/content/app/hooks/use-content-prefs.ts +55 -0
  178. package/corpus/templates/content/app/i18n-data.ts +170 -0
  179. package/corpus/templates/content/app/routes/_app.settings.tsx +47 -18
  180. package/corpus/templates/content/changelog/2026-07-30-breadcrumb-menus-now-hide-internal-system-pages.md +6 -0
  181. package/corpus/templates/content/changelog/2026-07-30-database-rows-can-now-be-removed-without-deleting-their-page.md +6 -0
  182. package/corpus/templates/content/changelog/2026-08-01-get-an-email-when-someone-comments-on-replies-in-or-mentions.md +6 -0
  183. package/corpus/templates/content/parity/eval-scenarios.ts +2 -2
  184. package/corpus/templates/content/parity/matrix.md +1 -1
  185. package/corpus/templates/content/parity/matrix.ts +4 -5
  186. package/corpus/templates/content/server/lib/comment-notifications.ts +146 -0
  187. package/corpus/templates/content/server/routes/_agent-native/content/user-prefs.get.ts +17 -0
  188. package/corpus/templates/content/server/routes/_agent-native/content/user-prefs.put.ts +36 -0
  189. package/corpus/templates/content/shared/api.ts +4 -0
  190. package/corpus/templates/content/shared/content-user-prefs.ts +11 -0
  191. package/corpus/templates/content/vitest.config.ts +20 -16
  192. package/corpus/templates/crm/app/routes/settings.tsx +14 -22
  193. package/corpus/templates/crm/vitest.config.ts +8 -4
  194. package/corpus/templates/design/.generated/bridge/editor-chrome.generated.ts +26 -11
  195. package/corpus/templates/design/actions/take-design-screenshot.ts +7 -0
  196. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +11 -3
  197. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +4 -1
  198. package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +54 -17
  199. package/corpus/templates/design/app/components/design/design-canvas/local-runtime.ts +126 -0
  200. package/corpus/templates/design/app/components/templates/TemplatePreview.tsx +2 -0
  201. package/corpus/templates/design/app/pages/Index.tsx +3 -1
  202. package/corpus/templates/design/app/pages/Present.tsx +2 -1
  203. package/corpus/templates/design/app/routes/settings.tsx +14 -22
  204. package/corpus/templates/design/changelog/2026-07-30-a-broken-inline-script-in-a-generated-screen-is-now-caught-o.md +6 -0
  205. package/corpus/templates/design/changelog/2026-07-30-design-html-is-now-validated-with-a-spec-grade-html-parser-i.md +6 -0
  206. package/corpus/templates/design/changelog/2026-07-30-designs-now-load-their-tailwind-and-alpine-runtimes-from-the.md +6 -0
  207. package/corpus/templates/design/changelog/2026-07-30-designs-with-a-broken-alpine-expression-are-now-caught-on-sa.md +6 -0
  208. package/corpus/templates/design/changelog/2026-07-30-editing-a-design-no-longer-strips-its-styling-the-canvas-kep.md +6 -0
  209. package/corpus/templates/design/changelog/2026-08-01-review-comments-replies-and-mentions-now-send-an-email-to-th.md +6 -0
  210. package/corpus/templates/design/package.json +4 -0
  211. package/corpus/templates/design/shared/html-integrity.ts +802 -556
  212. package/corpus/templates/design/vitest.config.ts +18 -12
  213. package/corpus/templates/dispatch/app/routes/settings.tsx +26 -40
  214. package/corpus/templates/dispatch/vitest.config.ts +11 -7
  215. package/corpus/templates/factory/app/routes/settings.tsx +14 -22
  216. package/corpus/templates/factory/vitest.config.ts +9 -0
  217. package/corpus/templates/forms/app/routes/_app.settings.tsx +14 -22
  218. package/corpus/templates/forms/vitest.config.ts +27 -23
  219. package/corpus/templates/macros/app/routes/settings.tsx +14 -22
  220. package/corpus/templates/macros/vitest.config.ts +16 -12
  221. package/corpus/templates/mail/app/pages/SettingsPage.tsx +14 -16
  222. package/corpus/templates/mail/vitest.config.ts +16 -12
  223. package/corpus/templates/plan/app/routes/settings.tsx +30 -44
  224. package/corpus/templates/plan/vitest.config.ts +24 -20
  225. package/corpus/templates/slides/actions/add-slide-comment.ts +12 -1
  226. package/corpus/templates/slides/app/components/ui/switch.tsx +1 -0
  227. package/corpus/templates/slides/app/hooks/use-slides-prefs.ts +55 -0
  228. package/corpus/templates/slides/app/i18n/en-US.ts +4 -0
  229. package/corpus/templates/slides/app/routes/settings.tsx +45 -22
  230. package/corpus/templates/slides/changelog/2026-08-01-get-an-email-when-someone-comments-on-or-replies-in-your-dec.md +6 -0
  231. package/corpus/templates/slides/server/lib/comment-notifications.ts +182 -0
  232. package/corpus/templates/slides/server/routes/_agent-native/slides/user-prefs.get.ts +16 -0
  233. package/corpus/templates/slides/server/routes/_agent-native/slides/user-prefs.put.ts +36 -0
  234. package/corpus/templates/slides/shared/slides-user-prefs.ts +11 -0
  235. package/corpus/templates/slides/vitest.config.ts +17 -13
  236. package/corpus/templates/tasks/app/components/layout/Sidebar.tsx +2 -0
  237. package/corpus/templates/tasks/app/routes/settings.tsx +14 -22
  238. package/corpus/templates/tasks/vitest.config.ts +17 -13
  239. package/corpus/toolkit/CHANGELOG.md +9 -0
  240. package/corpus/toolkit/package.json +1 -1
  241. package/corpus/toolkit/vitest.config.ts +3 -0
  242. package/dist/automations/service.d.ts +2 -0
  243. package/dist/automations/service.d.ts.map +1 -1
  244. package/dist/automations/service.js +29 -4
  245. package/dist/automations/service.js.map +1 -1
  246. package/dist/cli/mcp-config-writers.d.ts.map +1 -1
  247. package/dist/cli/mcp-config-writers.js +24 -23
  248. package/dist/cli/mcp-config-writers.js.map +1 -1
  249. package/dist/client/agent-page/AgentJobsTab.d.ts.map +1 -1
  250. package/dist/client/agent-page/AgentJobsTab.js +86 -29
  251. package/dist/client/agent-page/AgentJobsTab.js.map +1 -1
  252. package/dist/client/agent-page/AutomationDetailsDialog.d.ts +21 -0
  253. package/dist/client/agent-page/AutomationDetailsDialog.d.ts.map +1 -0
  254. package/dist/client/agent-page/AutomationDetailsDialog.js +33 -0
  255. package/dist/client/agent-page/AutomationDetailsDialog.js.map +1 -0
  256. package/dist/client/agent-page/AutomationScheduleDialog.d.ts +15 -0
  257. package/dist/client/agent-page/AutomationScheduleDialog.d.ts.map +1 -0
  258. package/dist/client/agent-page/AutomationScheduleDialog.js +46 -0
  259. package/dist/client/agent-page/AutomationScheduleDialog.js.map +1 -0
  260. package/dist/client/agent-page/TimezoneSelect.d.ts +17 -0
  261. package/dist/client/agent-page/TimezoneSelect.d.ts.map +1 -0
  262. package/dist/client/agent-page/TimezoneSelect.js +107 -0
  263. package/dist/client/agent-page/TimezoneSelect.js.map +1 -0
  264. package/dist/client/agent-page/use-jobs.d.ts +20 -7
  265. package/dist/client/agent-page/use-jobs.d.ts.map +1 -1
  266. package/dist/client/agent-page/use-jobs.js +24 -4
  267. package/dist/client/agent-page/use-jobs.js.map +1 -1
  268. package/dist/client/org/OrgSwitcher.d.ts +8 -1
  269. package/dist/client/org/OrgSwitcher.d.ts.map +1 -1
  270. package/dist/client/org/OrgSwitcher.js +8 -2
  271. package/dist/client/org/OrgSwitcher.js.map +1 -1
  272. package/dist/client/settings/AccountSettingsCard.d.ts.map +1 -1
  273. package/dist/client/settings/AccountSettingsCard.js +10 -9
  274. package/dist/client/settings/AccountSettingsCard.js.map +1 -1
  275. package/dist/client/settings/SchedulingTimezoneField.d.ts +10 -0
  276. package/dist/client/settings/SchedulingTimezoneField.d.ts.map +1 -0
  277. package/dist/client/settings/SchedulingTimezoneField.js +30 -0
  278. package/dist/client/settings/SchedulingTimezoneField.js.map +1 -0
  279. package/dist/client/settings/SettingsRow.d.ts +36 -0
  280. package/dist/client/settings/SettingsRow.d.ts.map +1 -0
  281. package/dist/client/settings/SettingsRow.js +15 -0
  282. package/dist/client/settings/SettingsRow.js.map +1 -0
  283. package/dist/client/settings/agent-settings-search.d.ts.map +1 -1
  284. package/dist/client/settings/agent-settings-search.js +1 -1
  285. package/dist/client/settings/agent-settings-search.js.map +1 -1
  286. package/dist/client/settings/index.d.ts +1 -0
  287. package/dist/client/settings/index.d.ts.map +1 -1
  288. package/dist/client/settings/index.js +1 -0
  289. package/dist/client/settings/index.js.map +1 -1
  290. package/dist/collab/struct-routes.d.ts +1 -1
  291. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  292. package/dist/integrations/identity.d.ts.map +1 -1
  293. package/dist/integrations/identity.js +2 -11
  294. package/dist/integrations/identity.js.map +1 -1
  295. package/dist/jobs/actions/list-automation-runs.d.ts +8 -0
  296. package/dist/jobs/actions/list-automation-runs.d.ts.map +1 -0
  297. package/dist/jobs/actions/list-automation-runs.js +29 -0
  298. package/dist/jobs/actions/list-automation-runs.js.map +1 -0
  299. package/dist/jobs/actions/list-recurring-jobs.d.ts +2 -0
  300. package/dist/jobs/actions/list-recurring-jobs.d.ts.map +1 -1
  301. package/dist/jobs/actions/list-recurring-jobs.js +20 -7
  302. package/dist/jobs/actions/list-recurring-jobs.js.map +1 -1
  303. package/dist/jobs/actions/manage-recurring-job.d.ts +2 -0
  304. package/dist/jobs/actions/manage-recurring-job.js +30 -9
  305. package/dist/jobs/actions/manage-recurring-job.js.map +1 -1
  306. package/dist/jobs/background-automation-runner.d.ts.map +1 -1
  307. package/dist/jobs/background-automation-runner.js +60 -0
  308. package/dist/jobs/background-automation-runner.js.map +1 -1
  309. package/dist/jobs/cron.d.ts +16 -2
  310. package/dist/jobs/cron.d.ts.map +1 -1
  311. package/dist/jobs/cron.js +43 -6
  312. package/dist/jobs/cron.js.map +1 -1
  313. package/dist/jobs/frontmatter.d.ts +12 -0
  314. package/dist/jobs/frontmatter.d.ts.map +1 -1
  315. package/dist/jobs/frontmatter.js +8 -0
  316. package/dist/jobs/frontmatter.js.map +1 -1
  317. package/dist/jobs/run-history.d.ts +56 -0
  318. package/dist/jobs/run-history.d.ts.map +1 -0
  319. package/dist/jobs/run-history.js +169 -0
  320. package/dist/jobs/run-history.js.map +1 -0
  321. package/dist/jobs/scheduler.d.ts.map +1 -1
  322. package/dist/jobs/scheduler.js +51 -18
  323. package/dist/jobs/scheduler.js.map +1 -1
  324. package/dist/jobs/tools.d.ts.map +1 -1
  325. package/dist/jobs/tools.js +37 -6
  326. package/dist/jobs/tools.js.map +1 -1
  327. package/dist/localization/actions/get-localization-preference.d.ts +1 -2
  328. package/dist/localization/actions/get-localization-preference.d.ts.map +1 -1
  329. package/dist/localization/actions/get-localization-preference.js.map +1 -1
  330. package/dist/localization/actions/set-localization-preference.d.ts +3 -3
  331. package/dist/localization/actions/set-localization-preference.d.ts.map +1 -1
  332. package/dist/localization/actions/set-localization-preference.js +33 -8
  333. package/dist/localization/actions/set-localization-preference.js.map +1 -1
  334. package/dist/localization/default-messages.d.ts +25 -0
  335. package/dist/localization/default-messages.d.ts.map +1 -1
  336. package/dist/localization/default-messages.js +25 -0
  337. package/dist/localization/default-messages.js.map +1 -1
  338. package/dist/localization/shared.d.ts +16 -1
  339. package/dist/localization/shared.d.ts.map +1 -1
  340. package/dist/localization/shared.js +22 -4
  341. package/dist/localization/shared.js.map +1 -1
  342. package/dist/localization/user-timezone.d.ts +11 -0
  343. package/dist/localization/user-timezone.d.ts.map +1 -0
  344. package/dist/localization/user-timezone.js +31 -0
  345. package/dist/localization/user-timezone.js.map +1 -0
  346. package/dist/notifications/routes.d.ts +2 -2
  347. package/dist/observability/routes.d.ts +1 -1
  348. package/dist/org/app-roles-handlers.d.ts.map +1 -1
  349. package/dist/org/app-roles-handlers.js +1 -8
  350. package/dist/org/app-roles-handlers.js.map +1 -1
  351. package/dist/org/index.d.ts +1 -0
  352. package/dist/org/index.d.ts.map +1 -1
  353. package/dist/org/index.js +1 -0
  354. package/dist/org/index.js.map +1 -1
  355. package/dist/org/membership.d.ts +6 -0
  356. package/dist/org/membership.d.ts.map +1 -0
  357. package/dist/org/membership.js +16 -0
  358. package/dist/org/membership.js.map +1 -0
  359. package/dist/provider-api/actions/custom-provider-registration.d.ts +10 -10
  360. package/dist/provider-api/actions/provider-api.d.ts +8 -8
  361. package/dist/provider-api/corpus-jobs.d.ts +2 -2
  362. package/dist/resources/handlers.d.ts +1 -1
  363. package/dist/review/actions/create-review-comment.d.ts +32 -1
  364. package/dist/review/actions/create-review-comment.d.ts.map +1 -1
  365. package/dist/review/actions/create-review-comment.js +3 -1
  366. package/dist/review/actions/create-review-comment.js.map +1 -1
  367. package/dist/review/actions/reply-review-comment.d.ts +32 -1
  368. package/dist/review/actions/reply-review-comment.d.ts.map +1 -1
  369. package/dist/review/actions/reply-review-comment.js +3 -1
  370. package/dist/review/actions/reply-review-comment.js.map +1 -1
  371. package/dist/review/index.d.ts +1 -0
  372. package/dist/review/index.d.ts.map +1 -1
  373. package/dist/review/index.js +1 -0
  374. package/dist/review/index.js.map +1 -1
  375. package/dist/review/notifications.d.ts +24 -0
  376. package/dist/review/notifications.d.ts.map +1 -0
  377. package/dist/review/notifications.js +123 -0
  378. package/dist/review/notifications.js.map +1 -0
  379. package/dist/review/types.d.ts +5 -0
  380. package/dist/review/types.d.ts.map +1 -1
  381. package/dist/review/types.js.map +1 -1
  382. package/dist/server/action-discovery.d.ts.map +1 -1
  383. package/dist/server/action-discovery.js +4 -0
  384. package/dist/server/action-discovery.js.map +1 -1
  385. package/dist/server/activity-notifications.d.ts +61 -0
  386. package/dist/server/activity-notifications.d.ts.map +1 -0
  387. package/dist/server/activity-notifications.js +103 -0
  388. package/dist/server/activity-notifications.js.map +1 -0
  389. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  390. package/dist/server/app-name.d.ts +18 -0
  391. package/dist/server/app-name.d.ts.map +1 -1
  392. package/dist/server/app-name.js +31 -0
  393. package/dist/server/app-name.js.map +1 -1
  394. package/dist/server/better-auth-instance.js +4 -4
  395. package/dist/server/better-auth-instance.js.map +1 -1
  396. package/dist/server/email-templates.d.ts +12 -0
  397. package/dist/server/email-templates.d.ts.map +1 -1
  398. package/dist/server/email-templates.js +57 -14
  399. package/dist/server/email-templates.js.map +1 -1
  400. package/dist/server/email.d.ts +12 -0
  401. package/dist/server/email.d.ts.map +1 -1
  402. package/dist/server/email.js +52 -5
  403. package/dist/server/email.js.map +1 -1
  404. package/dist/server/index.d.ts +1 -0
  405. package/dist/server/index.d.ts.map +1 -1
  406. package/dist/server/index.js +1 -0
  407. package/dist/server/index.js.map +1 -1
  408. package/dist/sharing/index.d.ts +1 -0
  409. package/dist/sharing/index.d.ts.map +1 -1
  410. package/dist/sharing/index.js +1 -0
  411. package/dist/sharing/index.js.map +1 -1
  412. package/dist/sharing/recipients.d.ts +31 -0
  413. package/dist/sharing/recipients.d.ts.map +1 -0
  414. package/dist/sharing/recipients.js +40 -0
  415. package/dist/sharing/recipients.js.map +1 -0
  416. package/dist/templates/chat/app/routes/settings.tsx +14 -22
  417. package/dist/templates/chat/vitest.config.ts +9 -0
  418. package/dist/triggers/actions/list-automations.d.ts +2 -0
  419. package/dist/triggers/actions/list-automations.d.ts.map +1 -1
  420. package/dist/triggers/actions/list-automations.js +19 -8
  421. package/dist/triggers/actions/list-automations.js.map +1 -1
  422. package/dist/triggers/actions/manage-automation.d.ts +6 -0
  423. package/dist/triggers/actions/manage-automation.js +12 -6
  424. package/dist/triggers/actions/manage-automation.js.map +1 -1
  425. package/dist/triggers/actions.d.ts.map +1 -1
  426. package/dist/triggers/actions.js +15 -4
  427. package/dist/triggers/actions.js.map +1 -1
  428. package/dist/triggers/dispatcher.d.ts.map +1 -1
  429. package/dist/triggers/dispatcher.js +19 -17
  430. package/dist/triggers/dispatcher.js.map +1 -1
  431. package/dist/triggers/routes.d.ts.map +1 -1
  432. package/dist/triggers/routes.js +6 -6
  433. package/dist/triggers/routes.js.map +1 -1
  434. package/dist/vitest-config.d.ts +10 -0
  435. package/dist/vitest-config.d.ts.map +1 -0
  436. package/dist/vitest-config.js +55 -0
  437. package/dist/vitest-config.js.map +1 -0
  438. package/docs/content/template-content-developers.mdx +1 -1
  439. package/package.json +5 -4
  440. package/src/automations/service.ts +41 -4
  441. package/src/cli/mcp-config-writers.ts +27 -21
  442. package/src/client/agent-page/AgentJobsTab.tsx +187 -85
  443. package/src/client/agent-page/AutomationDetailsDialog.tsx +208 -0
  444. package/src/client/agent-page/AutomationScheduleDialog.tsx +180 -0
  445. package/src/client/agent-page/TimezoneSelect.tsx +241 -0
  446. package/src/client/agent-page/use-jobs.ts +50 -10
  447. package/src/client/org/OrgSwitcher.tsx +26 -0
  448. package/src/client/settings/AccountSettingsCard.tsx +35 -31
  449. package/src/client/settings/SchedulingTimezoneField.tsx +71 -0
  450. package/src/client/settings/SettingsRow.tsx +113 -0
  451. package/src/client/settings/agent-settings-search.ts +2 -1
  452. package/src/client/settings/index.ts +6 -0
  453. package/src/integrations/identity.ts +2 -12
  454. package/src/jobs/actions/list-automation-runs.ts +33 -0
  455. package/src/jobs/actions/list-recurring-jobs.ts +27 -6
  456. package/src/jobs/actions/manage-recurring-job.ts +41 -9
  457. package/src/jobs/background-automation-runner.ts +88 -0
  458. package/src/jobs/cron.ts +52 -6
  459. package/src/jobs/frontmatter.ts +20 -0
  460. package/src/jobs/run-history.ts +242 -0
  461. package/src/jobs/scheduler.ts +73 -18
  462. package/src/jobs/tools.ts +54 -6
  463. package/src/localization/actions/get-localization-preference.ts +2 -2
  464. package/src/localization/actions/set-localization-preference.ts +49 -11
  465. package/src/localization/default-messages.ts +27 -0
  466. package/src/localization/shared.ts +39 -6
  467. package/src/localization/user-timezone.ts +36 -0
  468. package/src/org/app-roles-handlers.ts +1 -9
  469. package/src/org/index.ts +2 -0
  470. package/src/org/membership.ts +18 -0
  471. package/src/review/actions/create-review-comment.ts +4 -1
  472. package/src/review/actions/reply-review-comment.ts +4 -1
  473. package/src/review/index.ts +5 -0
  474. package/src/review/notifications.ts +163 -0
  475. package/src/review/types.ts +7 -0
  476. package/src/server/action-discovery.ts +4 -0
  477. package/src/server/activity-notifications.ts +174 -0
  478. package/src/server/app-name.ts +31 -0
  479. package/src/server/better-auth-instance.ts +4 -4
  480. package/src/server/email-templates.ts +70 -14
  481. package/src/server/email.ts +66 -3
  482. package/src/server/index.ts +10 -0
  483. package/src/sharing/index.ts +5 -0
  484. package/src/sharing/recipients.ts +67 -0
  485. package/src/templates/chat/app/routes/settings.tsx +14 -22
  486. package/src/templates/chat/vitest.config.ts +9 -0
  487. package/src/triggers/actions/list-automations.ts +27 -8
  488. package/src/triggers/actions/manage-automation.ts +17 -6
  489. package/src/triggers/actions.ts +16 -4
  490. package/src/triggers/dispatcher.ts +44 -30
  491. package/src/triggers/routes.ts +19 -6
  492. package/src/vitest-config.ts +68 -0
  493. package/corpus/templates/content/actions/delete-database-items.ts +0 -89
@@ -0,0 +1,169 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { getDbExec, intType, isPostgres } from "../db/client.js";
3
+ import { ensureIndexExists, ensureTableExists } from "../db/ddl-guard.js";
4
+ const TABLE = "automation_runs";
5
+ const MAX_ERROR_LENGTH = 500;
6
+ /**
7
+ * Generous multiple of the runner's own 5 minute hard abort
8
+ * (BACKGROUND_RUN_HARD_TIMEOUT_MS). Past this, no run is still alive.
9
+ */
10
+ const RUN_LIVENESS_CEILING_MS = 15 * 60_000;
11
+ /** Rows kept per automation, so a per-minute schedule cannot grow forever. */
12
+ const RUNS_RETAINED_PER_AUTOMATION = 50;
13
+ let _initPromise;
14
+ async function ensureTable() {
15
+ if (!_initPromise) {
16
+ _initPromise = (async () => {
17
+ const client = getDbExec();
18
+ const createSql = `
19
+ CREATE TABLE IF NOT EXISTS ${TABLE} (
20
+ id TEXT PRIMARY KEY,
21
+ owner TEXT NOT NULL,
22
+ automation TEXT NOT NULL,
23
+ path TEXT NOT NULL,
24
+ scope TEXT,
25
+ org_id TEXT,
26
+ run_id TEXT,
27
+ thread_id TEXT,
28
+ status TEXT NOT NULL DEFAULT 'running',
29
+ started_at ${intType()} NOT NULL,
30
+ finished_at ${intType()},
31
+ error TEXT
32
+ )
33
+ `;
34
+ const indexSql = `CREATE INDEX IF NOT EXISTS idx_${TABLE}_owner_automation ON ${TABLE} (owner, automation, started_at)`;
35
+ if (isPostgres()) {
36
+ await ensureTableExists(TABLE, createSql);
37
+ await ensureIndexExists(`idx_${TABLE}_owner_automation`, indexSql);
38
+ return;
39
+ }
40
+ await client.execute(createSql);
41
+ await client.execute(indexSql);
42
+ })().catch((err) => {
43
+ _initPromise = undefined;
44
+ throw err;
45
+ });
46
+ }
47
+ return _initPromise;
48
+ }
49
+ function toRun(row, now) {
50
+ const stored = String(row.status);
51
+ const startedAt = Number(row.started_at);
52
+ const status = stored === "running" && now - startedAt > RUN_LIVENESS_CEILING_MS
53
+ ? "interrupted"
54
+ : stored;
55
+ return {
56
+ id: String(row.id),
57
+ owner: String(row.owner),
58
+ automation: String(row.automation),
59
+ path: String(row.path),
60
+ scope: row.scope == null ? null : String(row.scope),
61
+ orgId: row.org_id == null ? null : String(row.org_id),
62
+ runId: row.run_id == null ? null : String(row.run_id),
63
+ threadId: row.thread_id == null ? null : String(row.thread_id),
64
+ status,
65
+ startedAt,
66
+ finishedAt: row.finished_at == null ? null : Number(row.finished_at),
67
+ error: row.error == null ? null : String(row.error),
68
+ };
69
+ }
70
+ /**
71
+ * Record that an automation actually began executing. Returns the run id used
72
+ * to close the record out. Only real executions get a row — a tick that
73
+ * declined to run the automation must not appear in its history.
74
+ */
75
+ export async function startAutomationRun(input) {
76
+ await ensureTable();
77
+ const id = randomUUID();
78
+ await getDbExec().execute({
79
+ sql: `INSERT INTO ${TABLE} (id, owner, automation, path, scope, org_id, run_id, thread_id, status, started_at)
80
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'running', ?)`,
81
+ args: [
82
+ id,
83
+ input.owner,
84
+ input.automation,
85
+ input.path,
86
+ input.scope ?? null,
87
+ input.orgId ?? null,
88
+ input.runId ?? null,
89
+ input.threadId ?? null,
90
+ Date.now(),
91
+ ],
92
+ });
93
+ await pruneAutomationRuns(input.owner, input.automation);
94
+ return id;
95
+ }
96
+ /**
97
+ * Drop the oldest rows for one automation once it exceeds the retention cap.
98
+ *
99
+ * Pruning on insert keeps the table bounded by the number of automations
100
+ * rather than by how often they run, and avoids a separate sweeper. The
101
+ * derived table is aliased because Postgres requires it.
102
+ */
103
+ async function pruneAutomationRuns(owner, automation) {
104
+ await getDbExec().execute({
105
+ sql: `DELETE FROM ${TABLE}
106
+ WHERE owner = ? AND automation = ? AND started_at < (
107
+ SELECT MIN(started_at) FROM (
108
+ SELECT started_at FROM ${TABLE}
109
+ WHERE owner = ? AND automation = ?
110
+ ORDER BY started_at DESC
111
+ LIMIT ${RUNS_RETAINED_PER_AUTOMATION}
112
+ ) recent
113
+ )`,
114
+ args: [owner, automation, owner, automation],
115
+ });
116
+ }
117
+ export async function finishAutomationRun(id, status, error) {
118
+ await ensureTable();
119
+ await getDbExec().execute({
120
+ sql: `UPDATE ${TABLE} SET status = ?, finished_at = ?, error = ? WHERE id = ?`,
121
+ args: [status, Date.now(), error?.slice(0, MAX_ERROR_LENGTH) ?? null, id],
122
+ });
123
+ }
124
+ /**
125
+ * Attach the agent thread once it exists. The thread is created after the run
126
+ * row so the history survives a crash between the two.
127
+ */
128
+ export async function attachAutomationRunThread(id, threadId, runId) {
129
+ await ensureTable();
130
+ await getDbExec().execute({
131
+ sql: `UPDATE ${TABLE} SET thread_id = ?, run_id = ? WHERE id = ?`,
132
+ args: [threadId, runId, id],
133
+ });
134
+ }
135
+ /**
136
+ * Forget an automation's executions.
137
+ *
138
+ * History is keyed by the automation's name, which is reusable: deleting
139
+ * "digest" and creating a new "digest" would otherwise show the old
140
+ * definition's runs as the new one's history.
141
+ */
142
+ export async function deleteAutomationRuns(owner, automation) {
143
+ await ensureTable();
144
+ // Bounded to runs that had already started. Names are reusable, so if a new
145
+ // automation takes this name and starts running before the cleanup lands,
146
+ // the cutoff keeps that run's history from being swept up with the old
147
+ // definition's.
148
+ const cutoff = Date.now();
149
+ await getDbExec().execute({
150
+ sql: `DELETE FROM ${TABLE} WHERE owner = ? AND automation = ? AND started_at <= ?`,
151
+ args: [owner, automation, cutoff],
152
+ });
153
+ }
154
+ export async function listAutomationRuns(options) {
155
+ await ensureTable();
156
+ const owners = options.owners.filter(Boolean);
157
+ if (!owners.length)
158
+ return [];
159
+ const limit = Math.min(Math.max(options.limit ?? 20, 1), 100);
160
+ const placeholders = owners.map(() => "?").join(", ");
161
+ const result = await getDbExec().execute({
162
+ sql: `SELECT * FROM ${TABLE} WHERE owner IN (${placeholders}) AND automation = ?
163
+ ORDER BY started_at DESC LIMIT ${limit}`,
164
+ args: [...owners, options.automation],
165
+ });
166
+ const now = Date.now();
167
+ return (result.rows ?? []).map((row) => toRun(row, now));
168
+ }
169
+ //# sourceMappingURL=run-history.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"run-history.js","sourceRoot":"","sources":["../../src/jobs/run-history.ts"],"names":[],"mappings":"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","sourcesContent":["import { randomUUID } from \"node:crypto\";\n\nimport { getDbExec, intType, isPostgres } from \"../db/client.js\";\nimport { ensureIndexExists, ensureTableExists } from \"../db/ddl-guard.js\";\n\n/**\n * \"interrupted\" is derived at read time, never stored: a process killed\n * mid-run cannot write its own outcome, so a row left running long past the\n * point a run could still be alive is reported as interrupted rather than\n * shown as permanently in-flight.\n */\nexport type AutomationRunStatus =\n | \"running\"\n | \"success\"\n | \"error\"\n | \"interrupted\";\n\nexport interface AutomationRun {\n id: string;\n owner: string;\n automation: string;\n path: string;\n scope: string | null;\n orgId: string | null;\n runId: string | null;\n threadId: string | null;\n status: AutomationRunStatus;\n startedAt: number;\n finishedAt: number | null;\n error: string | null;\n}\n\nexport interface StartAutomationRunInput {\n owner: string;\n automation: string;\n path: string;\n scope?: string | null;\n orgId?: string | null;\n runId?: string | null;\n threadId?: string | null;\n}\n\nconst TABLE = \"automation_runs\";\nconst MAX_ERROR_LENGTH = 500;\n\n/**\n * Generous multiple of the runner's own 5 minute hard abort\n * (BACKGROUND_RUN_HARD_TIMEOUT_MS). Past this, no run is still alive.\n */\nconst RUN_LIVENESS_CEILING_MS = 15 * 60_000;\n\n/** Rows kept per automation, so a per-minute schedule cannot grow forever. */\nconst RUNS_RETAINED_PER_AUTOMATION = 50;\n\nlet _initPromise: Promise<void> | undefined;\n\nasync function ensureTable(): Promise<void> {\n if (!_initPromise) {\n _initPromise = (async () => {\n const client = getDbExec();\n const createSql = `\n CREATE TABLE IF NOT EXISTS ${TABLE} (\n id TEXT PRIMARY KEY,\n owner TEXT NOT NULL,\n automation TEXT NOT NULL,\n path TEXT NOT NULL,\n scope TEXT,\n org_id TEXT,\n run_id TEXT,\n thread_id TEXT,\n status TEXT NOT NULL DEFAULT 'running',\n started_at ${intType()} NOT NULL,\n finished_at ${intType()},\n error TEXT\n )\n `;\n const indexSql = `CREATE INDEX IF NOT EXISTS idx_${TABLE}_owner_automation ON ${TABLE} (owner, automation, started_at)`;\n\n if (isPostgres()) {\n await ensureTableExists(TABLE, createSql);\n await ensureIndexExists(`idx_${TABLE}_owner_automation`, indexSql);\n return;\n }\n\n await client.execute(createSql);\n await client.execute(indexSql);\n })().catch((err) => {\n _initPromise = undefined;\n throw err;\n });\n }\n return _initPromise;\n}\n\nfunction toRun(row: Record<string, unknown>, now: number): AutomationRun {\n const stored = String(row.status) as AutomationRunStatus;\n const startedAt = Number(row.started_at);\n const status: AutomationRunStatus =\n stored === \"running\" && now - startedAt > RUN_LIVENESS_CEILING_MS\n ? \"interrupted\"\n : stored;\n return {\n id: String(row.id),\n owner: String(row.owner),\n automation: String(row.automation),\n path: String(row.path),\n scope: row.scope == null ? null : String(row.scope),\n orgId: row.org_id == null ? null : String(row.org_id),\n runId: row.run_id == null ? null : String(row.run_id),\n threadId: row.thread_id == null ? null : String(row.thread_id),\n status,\n startedAt,\n finishedAt: row.finished_at == null ? null : Number(row.finished_at),\n error: row.error == null ? null : String(row.error),\n };\n}\n\n/**\n * Record that an automation actually began executing. Returns the run id used\n * to close the record out. Only real executions get a row — a tick that\n * declined to run the automation must not appear in its history.\n */\nexport async function startAutomationRun(\n input: StartAutomationRunInput,\n): Promise<string> {\n await ensureTable();\n const id = randomUUID();\n await getDbExec().execute({\n sql: `INSERT INTO ${TABLE} (id, owner, automation, path, scope, org_id, run_id, thread_id, status, started_at)\n VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'running', ?)`,\n args: [\n id,\n input.owner,\n input.automation,\n input.path,\n input.scope ?? null,\n input.orgId ?? null,\n input.runId ?? null,\n input.threadId ?? null,\n Date.now(),\n ],\n });\n await pruneAutomationRuns(input.owner, input.automation);\n return id;\n}\n\n/**\n * Drop the oldest rows for one automation once it exceeds the retention cap.\n *\n * Pruning on insert keeps the table bounded by the number of automations\n * rather than by how often they run, and avoids a separate sweeper. The\n * derived table is aliased because Postgres requires it.\n */\nasync function pruneAutomationRuns(\n owner: string,\n automation: string,\n): Promise<void> {\n await getDbExec().execute({\n sql: `DELETE FROM ${TABLE}\n WHERE owner = ? AND automation = ? AND started_at < (\n SELECT MIN(started_at) FROM (\n SELECT started_at FROM ${TABLE}\n WHERE owner = ? AND automation = ?\n ORDER BY started_at DESC\n LIMIT ${RUNS_RETAINED_PER_AUTOMATION}\n ) recent\n )`,\n args: [owner, automation, owner, automation],\n });\n}\n\nexport async function finishAutomationRun(\n id: string,\n status: Exclude<AutomationRunStatus, \"running\">,\n error?: string,\n): Promise<void> {\n await ensureTable();\n await getDbExec().execute({\n sql: `UPDATE ${TABLE} SET status = ?, finished_at = ?, error = ? WHERE id = ?`,\n args: [status, Date.now(), error?.slice(0, MAX_ERROR_LENGTH) ?? null, id],\n });\n}\n\n/**\n * Attach the agent thread once it exists. The thread is created after the run\n * row so the history survives a crash between the two.\n */\nexport async function attachAutomationRunThread(\n id: string,\n threadId: string,\n runId: string,\n): Promise<void> {\n await ensureTable();\n await getDbExec().execute({\n sql: `UPDATE ${TABLE} SET thread_id = ?, run_id = ? WHERE id = ?`,\n args: [threadId, runId, id],\n });\n}\n\n/**\n * Forget an automation's executions.\n *\n * History is keyed by the automation's name, which is reusable: deleting\n * \"digest\" and creating a new \"digest\" would otherwise show the old\n * definition's runs as the new one's history.\n */\nexport async function deleteAutomationRuns(\n owner: string,\n automation: string,\n): Promise<void> {\n await ensureTable();\n // Bounded to runs that had already started. Names are reusable, so if a new\n // automation takes this name and starts running before the cleanup lands,\n // the cutoff keeps that run's history from being swept up with the old\n // definition's.\n const cutoff = Date.now();\n await getDbExec().execute({\n sql: `DELETE FROM ${TABLE} WHERE owner = ? AND automation = ? AND started_at <= ?`,\n args: [owner, automation, cutoff],\n });\n}\n\nexport async function listAutomationRuns(options: {\n owners: string[];\n automation: string;\n limit?: number;\n}): Promise<AutomationRun[]> {\n await ensureTable();\n const owners = options.owners.filter(Boolean);\n if (!owners.length) return [];\n const limit = Math.min(Math.max(options.limit ?? 20, 1), 100);\n const placeholders = owners.map(() => \"?\").join(\", \");\n const result = await getDbExec().execute({\n sql: `SELECT * FROM ${TABLE} WHERE owner IN (${placeholders}) AND automation = ?\n ORDER BY started_at DESC LIMIT ${limit}`,\n args: [...owners, options.automation],\n });\n const now = Date.now();\n return (result.rows ?? []).map((row) =>\n toRun(row as Record<string, unknown>, now),\n );\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"scheduler.d.ts","sourceRoot":"","sources":["../../src/jobs/scheduler.ts"],"names":[],"mappings":"AAKA,OAAO,EACL,yBAAyB,EAIzB,KAAK,2BAA2B,EAChC,KAAK,wBAAwB,EAC9B,MAAM,mCAAmC,CAAC;AAE3C,OAAO,EAGL,KAAK,cAAc,EACpB,MAAM,kBAAkB,CAAC;AAI1B,OAAO,EACL,sBAAsB,EACtB,mBAAmB,EACnB,oBAAoB,EACpB,gBAAgB,EAChB,KAAK,cAAc,EACnB,KAAK,yBAAyB,GAC/B,MAAM,kBAAkB,CAAC;AAE1B,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,GAAG;IACpD,IAAI,EAAE,cAAc,CAAC;IACrB,IAAI,EAAE,MAAM,CAAC;CACd,CAGA;AAED,wBAAgB,eAAe,CAAC,IAAI,EAAE,cAAc,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAE1E;AAID,MAAM,MAAM,mBAAmB,GAAG,2BAA2B,CAAC;AAE9D,MAAM,WAAW,aAAc,SAAQ,wBAAwB;IAC7D;;;;;;;;;;OAUG;IACH,mBAAmB,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,mBAAmB,KAAK,MAAM,EAAE,GAAG,SAAS,CAAC;CAC3E;AAgCD;;;GAGG;AACH,wBAAsB,oBAAoB,CAAC,IAAI,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CA0F7E;AAED,eAAO,MAAM,kBAAkB,kCAA4B,CAAC"}
1
+ {"version":3,"file":"scheduler.d.ts","sourceRoot":"","sources":["../../src/jobs/scheduler.ts"],"names":[],"mappings":"AAMA,OAAO,EACL,yBAAyB,EAIzB,KAAK,2BAA2B,EAChC,KAAK,wBAAwB,EAC9B,MAAM,mCAAmC,CAAC;AAO3C,OAAO,EAGL,KAAK,cAAc,EACpB,MAAM,kBAAkB,CAAC;AAI1B,OAAO,EACL,sBAAsB,EACtB,mBAAmB,EACnB,oBAAoB,EACpB,gBAAgB,EAChB,KAAK,cAAc,EACnB,KAAK,yBAAyB,GAC/B,MAAM,kBAAkB,CAAC;AAE1B,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,GAAG;IACpD,IAAI,EAAE,cAAc,CAAC;IACrB,IAAI,EAAE,MAAM,CAAC;CACd,CAGA;AAED,wBAAgB,eAAe,CAAC,IAAI,EAAE,cAAc,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAE1E;AAID,MAAM,MAAM,mBAAmB,GAAG,2BAA2B,CAAC;AAE9D,MAAM,WAAW,aAAc,SAAQ,wBAAwB;IAC7D;;;;;;;;;;OAUG;IACH,mBAAmB,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,mBAAmB,KAAK,MAAM,EAAE,GAAG,SAAS,CAAC;CAC3E;AAgCD;;;GAGG;AACH,wBAAsB,oBAAoB,CAAC,IAAI,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CA0F7E;AAED,eAAO,MAAM,kBAAkB,kCAA4B,CAAC"}
@@ -1,6 +1,6 @@
1
- import { resourceListAllOwners, resourcePut, } from "../resources/store.js";
1
+ import { resourceGetByPath, resourceListAllOwners, resourcePut, } from "../resources/store.js";
2
2
  import { backgroundRunCutOffReason, isBackgroundAutomationRunActive, resolveBackgroundAutomationIdentity, runBackgroundAutomation, } from "./background-automation-runner.js";
3
- import { nextOccurrence, isValidCron, describeCron } from "./cron.js";
3
+ import { nextOccurrence, isValidCron, describeCron, effectiveTimezone, } from "./cron.js";
4
4
  import { buildJobResourceContent, parseJobResource, } from "./frontmatter.js";
5
5
  // ─── Frontmatter parsing ────────────────────────────────────────────────────
6
6
  export { classifyJobFrontmatter, classifyJobResource, normalizeJobMcpTools, parseJobResource, } from "./frontmatter.js";
@@ -79,7 +79,7 @@ export async function processRecurringJobs(deps) {
79
79
  // Stuck — reset so the next check can re-run it
80
80
  meta.lastStatus = "error";
81
81
  meta.lastError = "Job timed out or server crashed mid-run";
82
- const next = nextOccurrence(meta.schedule, now);
82
+ const next = nextOccurrence(meta.schedule, now, meta.timezone);
83
83
  meta.nextRun = next.toISOString();
84
84
  await updateResource(resource, meta, body);
85
85
  continue;
@@ -95,7 +95,7 @@ export async function processRecurringJobs(deps) {
95
95
  // real next occurrence. Computing from new Date(0) (the epoch) always
96
96
  // returns a 1970 date, which is < now, so the job would fire
97
97
  // immediately on first sight regardless of its schedule.
98
- const next = nextOccurrence(meta.schedule, now);
98
+ const next = nextOccurrence(meta.schedule, now, meta.timezone);
99
99
  meta.nextRun = next.toISOString();
100
100
  await updateResource(resource, meta, body);
101
101
  continue;
@@ -146,10 +146,16 @@ async function executeJob(resource, meta, body, deps, now) {
146
146
  console.warn(`[recurring-jobs] Skipping job "${jobName}": ${identity.reason}. ` +
147
147
  `User/membership no longer valid — leaving cron entry for admin review.`);
148
148
  // Mark as skipped without resetting nextRun so an admin can find it.
149
- meta.lastRun = now.toISOString();
149
+ // `lastRun` is deliberately untouched: the job did not run, and stamping
150
+ // it here made a permanently blocked job look like it ran every minute.
151
+ // Re-writing an unchanged resource on every tick also churns the poll
152
+ // stream, so only persist when the failure state actually changed.
153
+ const alreadyRecorded = meta.lastStatus === "skipped" && meta.lastError === identity.reason;
154
+ meta.lastCheck = now.toISOString();
150
155
  meta.lastStatus = "skipped";
151
156
  meta.lastError = identity.reason;
152
- await updateResource(resource, meta, body);
157
+ if (!alreadyRecorded)
158
+ await updateResource(resource, meta, body);
153
159
  return;
154
160
  }
155
161
  const jobUserEmail = identity.identity.userEmail;
@@ -188,29 +194,56 @@ async function executeJob(resource, meta, body, deps, now) {
188
194
  automation: jobContext,
189
195
  ownerEmail: jobUserEmail,
190
196
  orgId: jobOrgId,
191
- prompt: `[Recurring Job: ${jobName}]\nSchedule: ${describeCron(meta.schedule)}\n\nExecute the following job instructions:\n\n${body}`,
197
+ prompt: `[Recurring Job: ${jobName}]\nSchedule: ${describeCron(meta.schedule, effectiveTimezone(meta.timezone))}\n\nExecute the following job instructions:\n\n${body}`,
192
198
  threadTitle: `Job: ${jobName} — ${now.toLocaleDateString()}`,
193
199
  runIdPrefix: `job-${jobName}`,
194
200
  usageLabel: `recurring-job:${jobName}`,
195
201
  requestContext,
196
202
  }, deps);
197
- // Compute from completion time so long runs cannot immediately re-fire.
198
- meta.lastStatus = "success";
199
- meta.nextRun = nextOccurrence(meta.schedule, new Date()).toISOString();
200
- await updateResource(resource, meta, body);
201
- console.log(`[recurring-jobs] Job "${jobName}" completed. Next run: ${meta.nextRun}`);
203
+ await recordExecutionOutcome(resource, {
204
+ lastRun: meta.lastRun,
205
+ lastStatus: "success",
206
+ lastError: undefined,
207
+ });
208
+ console.log(`[recurring-jobs] Job "${jobName}" completed.`);
202
209
  }
203
210
  catch (err) {
204
- meta.lastStatus = "error";
205
- meta.lastError =
206
- err instanceof Error ? err.message.slice(0, 200) : "Unknown error";
207
- meta.nextRun = nextOccurrence(meta.schedule, new Date()).toISOString();
208
- await updateResource(resource, meta, body);
209
- console.error(`[recurring-jobs] Job "${jobName}" failed:`, meta.lastError);
211
+ const lastError = err instanceof Error ? err.message.slice(0, 200) : "Unknown error";
212
+ await recordExecutionOutcome(resource, {
213
+ lastRun: meta.lastRun,
214
+ lastStatus: "error",
215
+ lastError,
216
+ });
217
+ console.error(`[recurring-jobs] Job "${jobName}" failed:`, lastError);
210
218
  }
211
219
  }
212
220
  async function updateResource(resource, meta, body) {
213
221
  const content = buildJobContent(meta, body);
214
222
  await resourcePut(resource.owner, resource.path, content);
215
223
  }
224
+ /**
225
+ * Persist the result of a run without clobbering a concurrent edit.
226
+ *
227
+ * A run holds its `meta` for as long as the job takes, so writing that whole
228
+ * snapshot back on completion would silently revert a schedule, timezone or
229
+ * instruction change made while it was running. Only the execution fields are
230
+ * ours to write, and `nextRun` is recomputed from whatever schedule is stored
231
+ * now, so an edit mid-run takes effect on the next tick.
232
+ */
233
+ async function recordExecutionOutcome(resource, outcome) {
234
+ const latest = await resourceGetByPath(resource.owner, resource.path);
235
+ if (!latest) {
236
+ // Deleted while it was running. Writing the pre-run snapshot back would
237
+ // resurrect the automation and schedule it again.
238
+ console.log(`[recurring-jobs] "${resource.path}" was deleted mid-run; dropping its outcome.`);
239
+ return;
240
+ }
241
+ const current = parseJobResource(latest.content);
242
+ const meta = { ...current.meta, ...outcome };
243
+ if (meta.schedule && isValidCron(meta.schedule)) {
244
+ // Measured from completion so a long run cannot immediately re-fire.
245
+ meta.nextRun = nextOccurrence(meta.schedule, new Date(), meta.timezone).toISOString();
246
+ }
247
+ await updateResource(resource, meta, current.body);
248
+ }
216
249
  //# sourceMappingURL=scheduler.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"scheduler.js","sourceRoot":"","sources":["../../src/jobs/scheduler.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n resourceListAllOwners,\n resourcePut,\n type Resource,\n} from \"../resources/store.js\";\nimport {\n backgroundRunCutOffReason,\n isBackgroundAutomationRunActive,\n resolveBackgroundAutomationIdentity,\n runBackgroundAutomation,\n type BackgroundAutomationContext,\n type BackgroundAutomationDeps,\n} from \"./background-automation-runner.js\";\nimport { nextOccurrence, isValidCron, describeCron } from \"./cron.js\";\nimport {\n buildJobResourceContent,\n parseJobResource,\n type JobFrontmatter,\n} from \"./frontmatter.js\";\n\n// ─── Frontmatter parsing ────────────────────────────────────────────────────\n\nexport {\n classifyJobFrontmatter,\n classifyJobResource,\n normalizeJobMcpTools,\n parseJobResource,\n type JobFrontmatter,\n type JobResourceClassification,\n} from \"./frontmatter.js\";\n\nexport function parseJobFrontmatter(content: string): {\n meta: JobFrontmatter;\n body: string;\n} {\n const { meta, body } = parseJobResource(content);\n return { meta, body };\n}\n\nexport function buildJobContent(meta: JobFrontmatter, body: string): string {\n return buildJobResourceContent(meta, body);\n}\n\n// ─── Job execution ──────────────────────────────────────────────────────────\n\nexport type RecurringJobContext = BackgroundAutomationContext;\n\nexport interface SchedulerDeps extends BackgroundAutomationDeps {\n /**\n * Tool names to expose on the FIRST engine request for a job run. When\n * provided, every other action returned by `getActions()` is deferred\n * behind an attached `tool-search` entry instead of being serialized on\n * every scheduled tick — `runAgentLoop`'s mid-run tool expansion\n * (`expandActiveTools`) still lets the model discover and call them after\n * a search. Omit to keep the full `getActions()` set visible up front\n * (current behavior). The caller (not this module) knows which of the\n * merged actions are the app's own vs. framework additions, so this must\n * be supplied explicitly rather than inferred here.\n */\n getInitialToolNames?: (job?: RecurringJobContext) => string[] | undefined;\n}\n\nlet _isRunning = false;\n\n// Skip the DB query on every tick if we recently confirmed no jobs exist.\n// `_hasJobsCache` is invalidated whenever a `jobs/*` resource is written or\n// deleted (subscribed below), and refreshed at most every 5 minutes.\nlet _hasJobsCache: boolean | undefined;\nlet _lastJobsCheck = 0;\nconst JOBS_CHECK_INTERVAL_MS = 5 * 60_000;\nlet _emitterSubscribed = false;\n\nfunction subscribeToJobsResourceEvents(): void {\n if (_emitterSubscribed) return;\n _emitterSubscribed = true;\n // Lazy import to avoid circular deps at module load\n import(\"../resources/emitter.js\")\n .then(({ getResourcesEmitter }) => {\n getResourcesEmitter().on(\"resources\", (event: any) => {\n if (typeof event?.path === \"string\" && event.path.startsWith(\"jobs/\")) {\n _hasJobsCache = undefined;\n }\n });\n })\n .catch((err) => {\n console.warn(\n \"[jobs] resource-event subscription failed:\",\n err instanceof Error ? err.message : err,\n );\n });\n}\n\n/**\n * Process all due recurring jobs. Called every 60 seconds.\n * Sequential execution with 5-minute timeout per job.\n */\nexport async function processRecurringJobs(deps: SchedulerDeps): Promise<void> {\n // Prevent concurrent runs\n if (_isRunning) return;\n\n subscribeToJobsResourceEvents();\n\n // Skip if we recently confirmed there are no job resources to run.\n const nowMs = Date.now();\n if (\n _hasJobsCache === false &&\n nowMs - _lastJobsCheck < JOBS_CHECK_INTERVAL_MS\n ) {\n return;\n }\n\n _isRunning = true;\n\n try {\n const jobResources = await resourceListAllOwners(\"jobs/\");\n _hasJobsCache = jobResources.some(\n (r) => r.path.endsWith(\".md\") && !r.path.endsWith(\".keep\"),\n );\n _lastJobsCheck = nowMs;\n if (!_hasJobsCache) return;\n const now = new Date();\n\n for (const resource of jobResources) {\n // Skip non-markdown or .keep files\n if (!resource.path.endsWith(\".md\")) continue;\n if (resource.path.endsWith(\".keep\")) continue;\n\n const { meta, body } = parseJobFrontmatter(resource.content);\n\n // Skip disabled or missing schedule\n if (!meta.enabled || !meta.schedule) continue;\n if (!isValidCron(meta.schedule)) continue;\n\n // Skip if currently running, unless it has been stuck for more than 10 minutes\n // (server crash mid-job leaves lastStatus=running forever without this guard)\n if (meta.lastStatus === \"running\") {\n if (isBackgroundAutomationRunActive(meta, now)) continue;\n // Stuck — reset so the next check can re-run it\n meta.lastStatus = \"error\";\n meta.lastError = \"Job timed out or server crashed mid-run\";\n const next = nextOccurrence(meta.schedule, now);\n meta.nextRun = next.toISOString();\n await updateResource(resource, meta, body);\n continue;\n }\n\n // Check if due\n if (meta.nextRun) {\n const nextRunDate = new Date(meta.nextRun);\n if (nextRunDate > now) continue;\n } else {\n // No nextRun computed yet — seed it from `now` so the job waits for its\n // real next occurrence. Computing from new Date(0) (the epoch) always\n // returns a 1970 date, which is < now, so the job would fire\n // immediately on first sight regardless of its schedule.\n const next = nextOccurrence(meta.schedule, now);\n meta.nextRun = next.toISOString();\n await updateResource(resource, meta, body);\n continue;\n }\n\n // Skip if body is empty\n if (!body.trim()) continue;\n\n // Execute the job\n await executeJob(resource, meta, body, deps, now);\n }\n } catch (err) {\n // Transient WS / connection drops (Neon serverless): silently retry next\n // tick instead of spamming stderr — `retryOnConnectionError` already did\n // its retry budget at the driver level.\n const { isConnectionError } = await import(\"../db/client.js\");\n if (isConnectionError(err)) {\n _hasJobsCache = undefined; // force re-check on next successful tick\n _lastJobsCheck = 0;\n return;\n }\n // Unwrap ErrorEvent (Neon WS driver emits these on network failure) so logs show the real cause\n const detail =\n err instanceof Error\n ? err\n : ((err as any)?.error ?? (err as any)?.message ?? err);\n console.error(\"[recurring-jobs] Error processing jobs:\", detail);\n } finally {\n _isRunning = false;\n }\n}\n\nexport const jobRunCutOffReason = backgroundRunCutOffReason;\n\nasync function executeJob(\n resource: Resource,\n meta: JobFrontmatter,\n body: string,\n deps: SchedulerDeps,\n now: Date,\n): Promise<void> {\n const jobName = resource.path.replace(/^jobs\\//, \"\").replace(/\\.md$/, \"\");\n\n const jobContext: RecurringJobContext = {\n name: jobName,\n meta,\n body,\n resource,\n };\n const identity = await resolveBackgroundAutomationIdentity(jobContext);\n\n // SECURITY (audit 12 #10): re-validate the run-as user/membership on\n // every tick. Sharing revocation, user deletion, and org-member removal\n // must take effect for already-scheduled jobs. Skip the tick on\n // failure; leave the cron entry alone so an admin can purge after\n // investigation.\n if (!identity.ok) {\n console.warn(\n `[recurring-jobs] Skipping job \"${jobName}\": ${identity.reason}. ` +\n `User/membership no longer valid — leaving cron entry for admin review.`,\n );\n // Mark as skipped without resetting nextRun so an admin can find it.\n meta.lastRun = now.toISOString();\n meta.lastStatus = \"skipped\";\n meta.lastError = identity.reason;\n await updateResource(resource, meta, body);\n return;\n }\n const jobUserEmail = identity.identity.userEmail;\n const jobOrgId = identity.identity.orgId;\n\n // Mark as running\n meta.lastRun = now.toISOString();\n meta.lastStatus = \"running\";\n meta.lastError = undefined;\n await updateResource(resource, meta, body);\n\n const requestContext =\n meta.originScopeId && meta.deliveryPlatform && meta.deliveryDestination\n ? {\n isIntegrationCaller: true as const,\n integration: {\n taskId: `job:${jobName}:${now.getTime()}`,\n scopeId: meta.originScopeId,\n principalType: \"service\" as const,\n incoming: {\n platform: meta.deliveryPlatform,\n externalThreadId: `${meta.deliveryTenantId || \"unknown\"}:${meta.deliveryDestination}:${meta.deliveryThreadRef || \"root\"}`,\n text: \"\",\n tenantId: meta.deliveryTenantId,\n integrationScopeId: meta.originScopeId,\n platformContext: {\n channelId: meta.deliveryDestination,\n threadTs: meta.deliveryThreadRef,\n teamId: meta.deliveryTenantId,\n },\n threadRef: meta.deliveryThreadRef,\n timestamp: now.getTime(),\n },\n },\n }\n : undefined;\n\n try {\n await runBackgroundAutomation(\n {\n automation: jobContext,\n ownerEmail: jobUserEmail,\n orgId: jobOrgId,\n prompt: `[Recurring Job: ${jobName}]\\nSchedule: ${describeCron(meta.schedule)}\\n\\nExecute the following job instructions:\\n\\n${body}`,\n threadTitle: `Job: ${jobName} — ${now.toLocaleDateString()}`,\n runIdPrefix: `job-${jobName}`,\n usageLabel: `recurring-job:${jobName}`,\n requestContext,\n },\n deps,\n );\n\n // Compute from completion time so long runs cannot immediately re-fire.\n meta.lastStatus = \"success\";\n meta.nextRun = nextOccurrence(meta.schedule, new Date()).toISOString();\n await updateResource(resource, meta, body);\n console.log(\n `[recurring-jobs] Job \"${jobName}\" completed. Next run: ${meta.nextRun}`,\n );\n } catch (err) {\n meta.lastStatus = \"error\";\n meta.lastError =\n err instanceof Error ? err.message.slice(0, 200) : \"Unknown error\";\n meta.nextRun = nextOccurrence(meta.schedule, new Date()).toISOString();\n await updateResource(resource, meta, body);\n console.error(`[recurring-jobs] Job \"${jobName}\" failed:`, meta.lastError);\n }\n}\n\nasync function updateResource(\n resource: Resource,\n meta: JobFrontmatter,\n body: string,\n): Promise<void> {\n const content = buildJobContent(meta, body);\n await resourcePut(resource.owner, resource.path, content);\n}\n"]}
1
+ {"version":3,"file":"scheduler.js","sourceRoot":"","sources":["../../src/jobs/scheduler.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n resourceGetByPath,\n resourceListAllOwners,\n resourcePut,\n type Resource,\n} from \"../resources/store.js\";\nimport {\n backgroundRunCutOffReason,\n isBackgroundAutomationRunActive,\n resolveBackgroundAutomationIdentity,\n runBackgroundAutomation,\n type BackgroundAutomationContext,\n type BackgroundAutomationDeps,\n} from \"./background-automation-runner.js\";\nimport {\n nextOccurrence,\n isValidCron,\n describeCron,\n effectiveTimezone,\n} from \"./cron.js\";\nimport {\n buildJobResourceContent,\n parseJobResource,\n type JobFrontmatter,\n} from \"./frontmatter.js\";\n\n// ─── Frontmatter parsing ────────────────────────────────────────────────────\n\nexport {\n classifyJobFrontmatter,\n classifyJobResource,\n normalizeJobMcpTools,\n parseJobResource,\n type JobFrontmatter,\n type JobResourceClassification,\n} from \"./frontmatter.js\";\n\nexport function parseJobFrontmatter(content: string): {\n meta: JobFrontmatter;\n body: string;\n} {\n const { meta, body } = parseJobResource(content);\n return { meta, body };\n}\n\nexport function buildJobContent(meta: JobFrontmatter, body: string): string {\n return buildJobResourceContent(meta, body);\n}\n\n// ─── Job execution ──────────────────────────────────────────────────────────\n\nexport type RecurringJobContext = BackgroundAutomationContext;\n\nexport interface SchedulerDeps extends BackgroundAutomationDeps {\n /**\n * Tool names to expose on the FIRST engine request for a job run. When\n * provided, every other action returned by `getActions()` is deferred\n * behind an attached `tool-search` entry instead of being serialized on\n * every scheduled tick — `runAgentLoop`'s mid-run tool expansion\n * (`expandActiveTools`) still lets the model discover and call them after\n * a search. Omit to keep the full `getActions()` set visible up front\n * (current behavior). The caller (not this module) knows which of the\n * merged actions are the app's own vs. framework additions, so this must\n * be supplied explicitly rather than inferred here.\n */\n getInitialToolNames?: (job?: RecurringJobContext) => string[] | undefined;\n}\n\nlet _isRunning = false;\n\n// Skip the DB query on every tick if we recently confirmed no jobs exist.\n// `_hasJobsCache` is invalidated whenever a `jobs/*` resource is written or\n// deleted (subscribed below), and refreshed at most every 5 minutes.\nlet _hasJobsCache: boolean | undefined;\nlet _lastJobsCheck = 0;\nconst JOBS_CHECK_INTERVAL_MS = 5 * 60_000;\nlet _emitterSubscribed = false;\n\nfunction subscribeToJobsResourceEvents(): void {\n if (_emitterSubscribed) return;\n _emitterSubscribed = true;\n // Lazy import to avoid circular deps at module load\n import(\"../resources/emitter.js\")\n .then(({ getResourcesEmitter }) => {\n getResourcesEmitter().on(\"resources\", (event: any) => {\n if (typeof event?.path === \"string\" && event.path.startsWith(\"jobs/\")) {\n _hasJobsCache = undefined;\n }\n });\n })\n .catch((err) => {\n console.warn(\n \"[jobs] resource-event subscription failed:\",\n err instanceof Error ? err.message : err,\n );\n });\n}\n\n/**\n * Process all due recurring jobs. Called every 60 seconds.\n * Sequential execution with 5-minute timeout per job.\n */\nexport async function processRecurringJobs(deps: SchedulerDeps): Promise<void> {\n // Prevent concurrent runs\n if (_isRunning) return;\n\n subscribeToJobsResourceEvents();\n\n // Skip if we recently confirmed there are no job resources to run.\n const nowMs = Date.now();\n if (\n _hasJobsCache === false &&\n nowMs - _lastJobsCheck < JOBS_CHECK_INTERVAL_MS\n ) {\n return;\n }\n\n _isRunning = true;\n\n try {\n const jobResources = await resourceListAllOwners(\"jobs/\");\n _hasJobsCache = jobResources.some(\n (r) => r.path.endsWith(\".md\") && !r.path.endsWith(\".keep\"),\n );\n _lastJobsCheck = nowMs;\n if (!_hasJobsCache) return;\n const now = new Date();\n\n for (const resource of jobResources) {\n // Skip non-markdown or .keep files\n if (!resource.path.endsWith(\".md\")) continue;\n if (resource.path.endsWith(\".keep\")) continue;\n\n const { meta, body } = parseJobFrontmatter(resource.content);\n\n // Skip disabled or missing schedule\n if (!meta.enabled || !meta.schedule) continue;\n if (!isValidCron(meta.schedule)) continue;\n\n // Skip if currently running, unless it has been stuck for more than 10 minutes\n // (server crash mid-job leaves lastStatus=running forever without this guard)\n if (meta.lastStatus === \"running\") {\n if (isBackgroundAutomationRunActive(meta, now)) continue;\n // Stuck — reset so the next check can re-run it\n meta.lastStatus = \"error\";\n meta.lastError = \"Job timed out or server crashed mid-run\";\n const next = nextOccurrence(meta.schedule, now, meta.timezone);\n meta.nextRun = next.toISOString();\n await updateResource(resource, meta, body);\n continue;\n }\n\n // Check if due\n if (meta.nextRun) {\n const nextRunDate = new Date(meta.nextRun);\n if (nextRunDate > now) continue;\n } else {\n // No nextRun computed yet — seed it from `now` so the job waits for its\n // real next occurrence. Computing from new Date(0) (the epoch) always\n // returns a 1970 date, which is < now, so the job would fire\n // immediately on first sight regardless of its schedule.\n const next = nextOccurrence(meta.schedule, now, meta.timezone);\n meta.nextRun = next.toISOString();\n await updateResource(resource, meta, body);\n continue;\n }\n\n // Skip if body is empty\n if (!body.trim()) continue;\n\n // Execute the job\n await executeJob(resource, meta, body, deps, now);\n }\n } catch (err) {\n // Transient WS / connection drops (Neon serverless): silently retry next\n // tick instead of spamming stderr — `retryOnConnectionError` already did\n // its retry budget at the driver level.\n const { isConnectionError } = await import(\"../db/client.js\");\n if (isConnectionError(err)) {\n _hasJobsCache = undefined; // force re-check on next successful tick\n _lastJobsCheck = 0;\n return;\n }\n // Unwrap ErrorEvent (Neon WS driver emits these on network failure) so logs show the real cause\n const detail =\n err instanceof Error\n ? err\n : ((err as any)?.error ?? (err as any)?.message ?? err);\n console.error(\"[recurring-jobs] Error processing jobs:\", detail);\n } finally {\n _isRunning = false;\n }\n}\n\nexport const jobRunCutOffReason = backgroundRunCutOffReason;\n\nasync function executeJob(\n resource: Resource,\n meta: JobFrontmatter,\n body: string,\n deps: SchedulerDeps,\n now: Date,\n): Promise<void> {\n const jobName = resource.path.replace(/^jobs\\//, \"\").replace(/\\.md$/, \"\");\n\n const jobContext: RecurringJobContext = {\n name: jobName,\n meta,\n body,\n resource,\n };\n const identity = await resolveBackgroundAutomationIdentity(jobContext);\n\n // SECURITY (audit 12 #10): re-validate the run-as user/membership on\n // every tick. Sharing revocation, user deletion, and org-member removal\n // must take effect for already-scheduled jobs. Skip the tick on\n // failure; leave the cron entry alone so an admin can purge after\n // investigation.\n if (!identity.ok) {\n console.warn(\n `[recurring-jobs] Skipping job \"${jobName}\": ${identity.reason}. ` +\n `User/membership no longer valid — leaving cron entry for admin review.`,\n );\n // Mark as skipped without resetting nextRun so an admin can find it.\n // `lastRun` is deliberately untouched: the job did not run, and stamping\n // it here made a permanently blocked job look like it ran every minute.\n // Re-writing an unchanged resource on every tick also churns the poll\n // stream, so only persist when the failure state actually changed.\n const alreadyRecorded =\n meta.lastStatus === \"skipped\" && meta.lastError === identity.reason;\n meta.lastCheck = now.toISOString();\n meta.lastStatus = \"skipped\";\n meta.lastError = identity.reason;\n if (!alreadyRecorded) await updateResource(resource, meta, body);\n return;\n }\n const jobUserEmail = identity.identity.userEmail;\n const jobOrgId = identity.identity.orgId;\n\n // Mark as running\n meta.lastRun = now.toISOString();\n meta.lastStatus = \"running\";\n meta.lastError = undefined;\n await updateResource(resource, meta, body);\n\n const requestContext =\n meta.originScopeId && meta.deliveryPlatform && meta.deliveryDestination\n ? {\n isIntegrationCaller: true as const,\n integration: {\n taskId: `job:${jobName}:${now.getTime()}`,\n scopeId: meta.originScopeId,\n principalType: \"service\" as const,\n incoming: {\n platform: meta.deliveryPlatform,\n externalThreadId: `${meta.deliveryTenantId || \"unknown\"}:${meta.deliveryDestination}:${meta.deliveryThreadRef || \"root\"}`,\n text: \"\",\n tenantId: meta.deliveryTenantId,\n integrationScopeId: meta.originScopeId,\n platformContext: {\n channelId: meta.deliveryDestination,\n threadTs: meta.deliveryThreadRef,\n teamId: meta.deliveryTenantId,\n },\n threadRef: meta.deliveryThreadRef,\n timestamp: now.getTime(),\n },\n },\n }\n : undefined;\n\n try {\n await runBackgroundAutomation(\n {\n automation: jobContext,\n ownerEmail: jobUserEmail,\n orgId: jobOrgId,\n prompt: `[Recurring Job: ${jobName}]\\nSchedule: ${describeCron(meta.schedule, effectiveTimezone(meta.timezone))}\\n\\nExecute the following job instructions:\\n\\n${body}`,\n threadTitle: `Job: ${jobName} — ${now.toLocaleDateString()}`,\n runIdPrefix: `job-${jobName}`,\n usageLabel: `recurring-job:${jobName}`,\n requestContext,\n },\n deps,\n );\n\n await recordExecutionOutcome(resource, {\n lastRun: meta.lastRun,\n lastStatus: \"success\",\n lastError: undefined,\n });\n console.log(`[recurring-jobs] Job \"${jobName}\" completed.`);\n } catch (err) {\n const lastError =\n err instanceof Error ? err.message.slice(0, 200) : \"Unknown error\";\n await recordExecutionOutcome(resource, {\n lastRun: meta.lastRun,\n lastStatus: \"error\",\n lastError,\n });\n console.error(`[recurring-jobs] Job \"${jobName}\" failed:`, lastError);\n }\n}\n\nasync function updateResource(\n resource: Resource,\n meta: JobFrontmatter,\n body: string,\n): Promise<void> {\n const content = buildJobContent(meta, body);\n await resourcePut(resource.owner, resource.path, content);\n}\n\n/** Execution bookkeeping the scheduler owns; the rest belongs to the editor. */\ntype ExecutionOutcome = Pick<\n JobFrontmatter,\n \"lastRun\" | \"lastCheck\" | \"lastStatus\" | \"lastError\"\n>;\n\n/**\n * Persist the result of a run without clobbering a concurrent edit.\n *\n * A run holds its `meta` for as long as the job takes, so writing that whole\n * snapshot back on completion would silently revert a schedule, timezone or\n * instruction change made while it was running. Only the execution fields are\n * ours to write, and `nextRun` is recomputed from whatever schedule is stored\n * now, so an edit mid-run takes effect on the next tick.\n */\nasync function recordExecutionOutcome(\n resource: Resource,\n outcome: ExecutionOutcome,\n): Promise<void> {\n const latest = await resourceGetByPath(resource.owner, resource.path);\n if (!latest) {\n // Deleted while it was running. Writing the pre-run snapshot back would\n // resurrect the automation and schedule it again.\n console.log(\n `[recurring-jobs] \"${resource.path}\" was deleted mid-run; dropping its outcome.`,\n );\n return;\n }\n const current = parseJobResource(latest.content);\n\n const meta: JobFrontmatter = { ...current.meta, ...outcome };\n if (meta.schedule && isValidCron(meta.schedule)) {\n // Measured from completion so a long run cannot immediately re-fire.\n meta.nextRun = nextOccurrence(\n meta.schedule,\n new Date(),\n meta.timezone,\n ).toISOString();\n }\n await updateResource(resource, meta, current.body);\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../../src/jobs/tools.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAC;AAkBhE,OAAO,EAIL,KAAK,cAAc,EACpB,MAAM,gBAAgB,CAAC;AAsCxB;;;;;;;;;;GAUG;AACH,wBAAsB,oBAAoB,CACxC,aAAa,EAAE,MAAM,EACrB,IAAI,EAAE,cAAc,GACnB,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAkBxB;AAoOD,wBAAgB,cAAc,IAAI,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CA8F5D"}
1
+ {"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../../src/jobs/tools.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,8BAA8B,CAAC;AAyBhE,OAAO,EAIL,KAAK,cAAc,EACpB,MAAM,gBAAgB,CAAC;AAsCxB;;;;;;;;;;GAUG;AACH,wBAAsB,oBAAoB,CACxC,aAAa,EAAE,MAAM,EACrB,IAAI,EAAE,cAAc,GACnB,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAkBxB;AAwQD,wBAAgB,cAAc,IAAI,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAmG5D"}
@@ -1,7 +1,8 @@
1
1
  import { getDbExec } from "../db/client.js";
2
+ import { resolveUserSchedulingTimezone } from "../localization/user-timezone.js";
2
3
  import { resourcePut, resourceGetByPath, resourceList, resourceDelete, organizationIdFromResourceOwner, sharedResourceOwner, SHARED_OWNER, } from "../resources/store.js";
3
4
  import { getRequestUserEmail, getRequestOrgId, getIntegrationRequestContext, } from "../server/request-context.js";
4
- import { isValidCron, nextOccurrence, describeCron } from "./cron.js";
5
+ import { isValidCron, nextOccurrence, describeCron, effectiveTimezone, isValidTimezone, } from "./cron.js";
5
6
  import { classifyJobResource } from "./frontmatter.js";
6
7
  import { parseJobFrontmatter, buildJobContent, normalizeJobMcpTools, } from "./scheduler.js";
7
8
  function getOwner() {
@@ -70,6 +71,7 @@ export async function authorizeJobMutation(resourceOwner, meta) {
70
71
  }
71
72
  async function runCreate(args) {
72
73
  const { name, schedule, instructions, scope, runAs, model } = args;
74
+ const requestedTimezone = args.timezone;
73
75
  if (!name || !schedule || !instructions) {
74
76
  return JSON.stringify({
75
77
  error: "name, schedule, and instructions are required",
@@ -90,12 +92,22 @@ async function runCreate(args) {
90
92
  const owner = scope === "personal" ? getOwner() : getSharedOwner();
91
93
  const path = `jobs/${name}.md`;
92
94
  const now = new Date();
93
- const next = nextOccurrence(schedule, now);
95
+ // A cron time with no zone silently means the host's zone, which is how an
96
+ // "8am" job ends up firing at 4am for the person who asked for it.
97
+ if (requestedTimezone && !isValidTimezone(requestedTimezone)) {
98
+ return JSON.stringify({
99
+ error: `Unknown timezone: "${requestedTimezone}". Use an IANA zone such as America/New_York.`,
100
+ });
101
+ }
102
+ const timezone = requestedTimezone ||
103
+ (await resolveUserSchedulingTimezone(getRequestUserEmail()));
104
+ const next = nextOccurrence(schedule, now, timezone);
94
105
  const integration = getIntegrationRequestContext();
95
106
  const channelId = integration?.incoming.platformContext.channelId;
96
107
  const threadRef = integration?.incoming.threadRef;
97
108
  const meta = {
98
109
  schedule,
110
+ timezone,
99
111
  enabled: true,
100
112
  createdBy: getOwner(),
101
113
  orgId: getRequestOrgId() || undefined,
@@ -124,7 +136,8 @@ async function runCreate(args) {
124
136
  name,
125
137
  path,
126
138
  schedule,
127
- scheduleDescription: describeCron(schedule),
139
+ timezone,
140
+ scheduleDescription: describeCron(schedule, timezone),
128
141
  nextRun: next.toISOString(),
129
142
  scope: scope || "shared",
130
143
  ...(mcpTools?.length ? { mcpTools } : {}),
@@ -156,7 +169,10 @@ async function runList(args) {
156
169
  path: r.path,
157
170
  scope: r.owner === sharedOwner ? "shared" : "personal",
158
171
  schedule: meta.schedule,
159
- scheduleDescription: meta.schedule ? describeCron(meta.schedule) : "",
172
+ timezone: effectiveTimezone(meta.timezone),
173
+ scheduleDescription: meta.schedule
174
+ ? describeCron(meta.schedule, effectiveTimezone(meta.timezone))
175
+ : "",
160
176
  enabled: meta.enabled,
161
177
  lastRun: meta.lastRun || null,
162
178
  lastStatus: meta.lastStatus || null,
@@ -208,7 +224,17 @@ async function runUpdate(args) {
208
224
  });
209
225
  }
210
226
  meta.schedule = schedule;
211
- meta.nextRun = nextOccurrence(schedule).toISOString();
227
+ }
228
+ if (args.timezone !== undefined) {
229
+ if (!isValidTimezone(args.timezone)) {
230
+ return JSON.stringify({
231
+ error: `Unknown timezone: "${args.timezone}".`,
232
+ });
233
+ }
234
+ meta.timezone = args.timezone;
235
+ }
236
+ if (schedule || args.timezone !== undefined) {
237
+ meta.nextRun = nextOccurrence(meta.schedule, undefined, meta.timezone).toISOString();
212
238
  }
213
239
  if (enabled !== undefined) {
214
240
  // Accept both the schema's string enum ("true"/"false") and a real boolean
@@ -240,7 +266,8 @@ async function runUpdate(args) {
240
266
  updated: true,
241
267
  name,
242
268
  schedule: meta.schedule,
243
- scheduleDescription: describeCron(meta.schedule),
269
+ timezone: effectiveTimezone(meta.timezone),
270
+ scheduleDescription: describeCron(meta.schedule, effectiveTimezone(meta.timezone)),
244
271
  enabled: meta.enabled,
245
272
  nextRun: meta.nextRun,
246
273
  mcpTools: meta.mcpTools || [],
@@ -299,6 +326,10 @@ For jobs that use a connected MCP, pass the exact tool names in mcpTools. This b
299
326
  type: "string",
300
327
  description: "Job name (hyphen-case, e.g. 'daily-scorecard-check'). Required for create and update.",
301
328
  },
329
+ timezone: {
330
+ type: "string",
331
+ description: "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.",
332
+ },
302
333
  schedule: {
303
334
  type: "string",
304
335
  description: "Cron expression (5 fields: minute hour day-of-month month day-of-week). Required for create, optional for update.",