@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
@@ -4,14 +4,46 @@ function escapeLike(value: string): string {
4
4
  return value.replace(/[!%_]/g, (match) => `!${match}`);
5
5
  }
6
6
 
7
- function chunkPrefix(recordingId: string): string {
8
- return `recording-chunks-${recordingId}-`;
7
+ function chunkPrefix(
8
+ recordingId: string,
9
+ generationId?: string | null,
10
+ ): string {
11
+ return generationId
12
+ ? `recording-chunks-${recordingId}-${generationId}-`
13
+ : `recording-chunks-${recordingId}-`;
9
14
  }
10
15
 
11
16
  function likePrefix(prefix: string): string {
12
17
  return `${escapeLike(prefix)}%`;
13
18
  }
14
19
 
20
+ function exactChunkKeyArgs(
21
+ ownerEmail: string,
22
+ recordingId: string,
23
+ generationId?: string | null,
24
+ ): [string, string, number, string, string] {
25
+ const prefix = chunkPrefix(recordingId, generationId);
26
+ return [
27
+ ownerEmail,
28
+ likePrefix(prefix),
29
+ prefix.length + 6,
30
+ `${prefix}000000`,
31
+ `${prefix}999999`,
32
+ ];
33
+ }
34
+
35
+ const exactChunkKeyWhere = `session_id = ? AND key LIKE ? ESCAPE '!' AND length(key) = ? AND key >= ? AND key <= ?`;
36
+
37
+ function isChunkKeyForGeneration(
38
+ key: string,
39
+ recordingId: string,
40
+ generationId?: string | null,
41
+ ): boolean {
42
+ const prefix = chunkPrefix(recordingId, generationId);
43
+ if (!key.startsWith(prefix)) return false;
44
+ return /^\d+$/.test(key.slice(prefix.length));
45
+ }
46
+
15
47
  function numberFromRowValue(value: unknown): number {
16
48
  if (typeof value === "number" && Number.isFinite(value)) return value;
17
49
  if (typeof value === "bigint") return Number(value);
@@ -81,21 +113,25 @@ export function validateRecordingChunkKeys(
81
113
  export async function listRecordingChunkKeys(
82
114
  ownerEmail: string,
83
115
  recordingId: string,
116
+ generationId?: string | null,
84
117
  ): Promise<string[]> {
85
118
  const { rows } = await getDbExec().execute({
86
- sql: `SELECT key FROM application_state WHERE session_id = ? AND key LIKE ? ESCAPE '!'`,
87
- args: [ownerEmail, likePrefix(chunkPrefix(recordingId))],
119
+ sql: `SELECT key FROM application_state WHERE ${exactChunkKeyWhere}`,
120
+ args: exactChunkKeyArgs(ownerEmail, recordingId, generationId),
88
121
  });
89
- return rows.map((row) => String(row.key));
122
+ return rows
123
+ .map((row) => String(row.key))
124
+ .filter((key) => isChunkKeyForGeneration(key, recordingId, generationId));
90
125
  }
91
126
 
92
127
  export async function deleteRecordingChunks(
93
128
  ownerEmail: string,
94
129
  recordingId: string,
130
+ generationId?: string | null,
95
131
  ): Promise<number> {
96
132
  const result = await getDbExec().execute({
97
- sql: `DELETE FROM application_state WHERE session_id = ? AND key LIKE ? ESCAPE '!'`,
98
- args: [ownerEmail, likePrefix(chunkPrefix(recordingId))],
133
+ sql: `DELETE FROM application_state WHERE ${exactChunkKeyWhere}`,
134
+ args: exactChunkKeyArgs(ownerEmail, recordingId, generationId),
99
135
  });
100
136
  return result.rowsAffected ?? 0;
101
137
  }
@@ -103,13 +139,14 @@ export async function deleteRecordingChunks(
103
139
  export async function sumRecordingChunkBytes(
104
140
  ownerEmail: string,
105
141
  recordingId: string,
142
+ generationId?: string | null,
106
143
  ): Promise<number> {
107
144
  const bytesExpression = isPostgres()
108
145
  ? `COALESCE(SUM((value::jsonb ->> 'bytes')::bigint), 0)`
109
146
  : `COALESCE(SUM(json_extract(value, '$.bytes')), 0)`;
110
147
  const { rows } = await getDbExec().execute({
111
- sql: `SELECT ${bytesExpression} AS bytes FROM application_state WHERE session_id = ? AND key LIKE ? ESCAPE '!'`,
112
- args: [ownerEmail, likePrefix(chunkPrefix(recordingId))],
148
+ sql: `SELECT ${bytesExpression} AS bytes FROM application_state WHERE ${exactChunkKeyWhere}`,
149
+ args: exactChunkKeyArgs(ownerEmail, recordingId, generationId),
113
150
  });
114
151
  return numberFromRowValue(rows[0]?.bytes);
115
152
  }
@@ -5,9 +5,14 @@ import {
5
5
  getRequestUserEmail,
6
6
  getRequestOrgId,
7
7
  } from "@agent-native/core/server/request-context";
8
+ import { getUserSetting } from "@agent-native/core/settings";
8
9
  import { and, count, desc, eq, sql } from "drizzle-orm";
9
10
  import { HTTPError, type H3Event } from "h3";
10
11
 
12
+ import {
13
+ CLIPS_USER_PREFS_KEY,
14
+ type ClipsUserPrefs,
15
+ } from "../../shared/clips-ai-prefs.js";
11
16
  import { getDb, schema } from "../db/index.js";
12
17
 
13
18
  export function getCurrentOwnerEmail(): string {
@@ -99,6 +104,25 @@ export async function getOrganizationDefaultVisibility(
99
104
  }
100
105
  }
101
106
 
107
+ /**
108
+ * Visibility for a new recording: the personal preference of the creator
109
+ * wins, then the organization default, then the built-in default.
110
+ */
111
+ export async function getDefaultRecordingVisibility(
112
+ organizationId: string | null | undefined,
113
+ ): Promise<RecordingVisibility> {
114
+ const email = getRequestUserEmail();
115
+ if (email) {
116
+ const prefs = (await getUserSetting(
117
+ normalizeOwnerEmail(email),
118
+ CLIPS_USER_PREFS_KEY,
119
+ )) as ClipsUserPrefs | null;
120
+ const preferred = prefs?.defaultRecordingVisibility;
121
+ if (isRecordingVisibility(preferred)) return preferred;
122
+ }
123
+ return getOrganizationDefaultVisibility(organizationId);
124
+ }
125
+
102
126
  const ORG_ROLE_RANK: Record<OrganizationAccessRole, number> = {
103
127
  member: 1,
104
128
  admin: 2,
@@ -12,12 +12,16 @@ export interface StoredResumableSession {
12
12
  lastCommittedIndex?: number;
13
13
  }
14
14
 
15
- const key = (recordingId: string) => `resumable-session-${recordingId}`;
15
+ const key = (recordingId: string, generationId?: string | null) =>
16
+ generationId
17
+ ? `resumable-session-${recordingId}-${generationId}`
18
+ : `resumable-session-${recordingId}`;
16
19
 
17
20
  export async function getResumableSession(
18
21
  recordingId: string,
22
+ generationId?: string | null,
19
23
  ): Promise<StoredResumableSession | null> {
20
- const raw = await readAppState(key(recordingId));
24
+ const raw = await readAppState(key(recordingId, generationId));
21
25
  if (!raw || typeof raw !== "object") return null;
22
26
  return raw as unknown as StoredResumableSession;
23
27
  }
@@ -25,15 +29,17 @@ export async function getResumableSession(
25
29
  export async function setResumableSession(
26
30
  recordingId: string,
27
31
  session: StoredResumableSession,
32
+ generationId?: string | null,
28
33
  ): Promise<void> {
29
34
  await writeAppState(
30
- key(recordingId),
35
+ key(recordingId, generationId),
31
36
  session as unknown as Record<string, unknown>,
32
37
  );
33
38
  }
34
39
 
35
40
  export async function deleteResumableSession(
36
41
  recordingId: string,
42
+ generationId?: string | null,
37
43
  ): Promise<void> {
38
- await deleteAppState(key(recordingId));
44
+ await deleteAppState(key(recordingId, generationId));
39
45
  }
@@ -9,6 +9,8 @@ const CLIPS_BRAND_NAME = "Clips";
9
9
  const EMAIL_SEND_TIMEOUT_MS = 60_000;
10
10
  const FRIENDLY_REPLY_TO = "hello@agent-native.com";
11
11
  const UNTITLED_CLIP = "Untitled Clip";
12
+ const ACTIVITY_EMAIL_FOOTER =
13
+ "You received this because email notifications are on in your Clips settings.";
12
14
 
13
15
  interface TransactionalEmailBase {
14
16
  to: string;
@@ -37,6 +39,25 @@ export type ClipsTransactionalEmailInput =
37
39
  | (TransactionalEmailBase & {
38
40
  kind: "two-clips";
39
41
  generatedSummary?: string | null;
42
+ })
43
+ | (TransactionalEmailBase & {
44
+ kind: "activity-comment";
45
+ recordingId: string;
46
+ title?: string | null;
47
+ authorEmail?: string | null;
48
+ authorName?: string | null;
49
+ content: string;
50
+ videoTimestampMs?: number | null;
51
+ isReply?: boolean;
52
+ })
53
+ | (TransactionalEmailBase & {
54
+ kind: "activity-reaction";
55
+ recordingId: string;
56
+ title?: string | null;
57
+ emoji: string;
58
+ authorEmail?: string | null;
59
+ authorName?: string | null;
60
+ videoTimestampMs?: number | null;
40
61
  });
41
62
 
42
63
  export interface ClipsTransactionalEmailRenderOptions {
@@ -103,6 +124,31 @@ function clipUrl(
103
124
  return appUrlForPath(`/r/${encodeURIComponent(recordingId)}`, options);
104
125
  }
105
126
 
127
+ function clipCommentsUrl(
128
+ recordingId: string,
129
+ videoTimestampMs: number | null | undefined,
130
+ options: ClipsTransactionalEmailRenderOptions,
131
+ ): string {
132
+ const url = new URL(clipUrl(recordingId, options));
133
+ url.searchParams.set("panel", "comments");
134
+ if (typeof videoTimestampMs === "number" && videoTimestampMs > 0) {
135
+ url.searchParams.set("t", String(Math.floor(videoTimestampMs / 1000)));
136
+ }
137
+ return url.toString();
138
+ }
139
+
140
+ function formatTimestamp(ms: number): string {
141
+ const total = Math.floor(ms / 1000);
142
+ const minutes = Math.floor(total / 60);
143
+ const seconds = total % 60;
144
+ return `${minutes}:${String(seconds).padStart(2, "0")}`;
145
+ }
146
+
147
+ function quotedExcerpt(value: string): string {
148
+ const text = singleLine(value);
149
+ return text.length > 280 ? `${text.slice(0, 277)}…` : text;
150
+ }
151
+
106
152
  function recordUrl(options: ClipsTransactionalEmailRenderOptions): string {
107
153
  return appUrlForPath("/record", options);
108
154
  }
@@ -232,6 +278,69 @@ export function renderClipsTransactionalEmail(
232
278
  });
233
279
  return { subject, ...rendered };
234
280
  }
281
+
282
+ case "activity-comment": {
283
+ const author =
284
+ singleLine(input.authorName) ||
285
+ normalizeEmailDisplayName(input.authorEmail, "Someone");
286
+ const at =
287
+ typeof input.videoTimestampMs === "number" && input.videoTimestampMs > 0
288
+ ? ` at ${formatTimestamp(input.videoTimestampMs)}`
289
+ : "";
290
+ const subject = input.isReply
291
+ ? `${author} replied on “${title}”`
292
+ : `${author} commented on “${title}”`;
293
+ const rendered = renderEmail({
294
+ brandName: CLIPS_BRAND_NAME,
295
+ preheader: subject,
296
+ heading: input.isReply
297
+ ? `${author} replied on your Clip`
298
+ : `${author} commented on your Clip`,
299
+ paragraphs: [
300
+ `${emailStrong(author)} left a ${input.isReply ? "reply" : "comment"} on ${emailStrong(title!)}${at}.`,
301
+ `“${quotedExcerpt(input.content)}”`,
302
+ ],
303
+ cta: {
304
+ label: "Read and reply",
305
+ url: clipCommentsUrl(
306
+ input.recordingId,
307
+ input.videoTimestampMs,
308
+ options,
309
+ ),
310
+ },
311
+ footer: ACTIVITY_EMAIL_FOOTER,
312
+ });
313
+ return { subject, ...rendered };
314
+ }
315
+
316
+ case "activity-reaction": {
317
+ const author =
318
+ singleLine(input.authorName) ||
319
+ normalizeEmailDisplayName(input.authorEmail, "Someone");
320
+ const at =
321
+ typeof input.videoTimestampMs === "number" && input.videoTimestampMs > 0
322
+ ? ` at ${formatTimestamp(input.videoTimestampMs)}`
323
+ : "";
324
+ const subject = `${author} reacted ${input.emoji} on “${title}”`;
325
+ const rendered = renderEmail({
326
+ brandName: CLIPS_BRAND_NAME,
327
+ preheader: subject,
328
+ heading: "Someone reacted to your Clip",
329
+ paragraphs: [
330
+ `${emailStrong(author)} reacted ${emailStrong(input.emoji)} on ${emailStrong(title!)}${at}.`,
331
+ ],
332
+ cta: {
333
+ label: "See Clip activity",
334
+ url: clipCommentsUrl(
335
+ input.recordingId,
336
+ input.videoTimestampMs,
337
+ options,
338
+ ),
339
+ },
340
+ footer: ACTIVITY_EMAIL_FOOTER,
341
+ });
342
+ return { subject, ...rendered };
343
+ }
235
344
  }
236
345
  }
237
346
 
@@ -0,0 +1,8 @@
1
+ export const RETRYABLE_UPLOAD_INTERRUPTION_REASON =
2
+ "Upload was interrupted. The local recording is safe; retry from the Clips desktop app.";
3
+
4
+ export function isRetryableUploadInterruption(
5
+ failureReason: string | null | undefined,
6
+ ): boolean {
7
+ return failureReason === RETRYABLE_UPLOAD_INTERRUPTION_REASON;
8
+ }
@@ -12,7 +12,7 @@
12
12
  */
13
13
 
14
14
  import { getDbExec, isPostgres } from "@agent-native/core/db";
15
- import { and, eq, inArray, sql } from "drizzle-orm";
15
+ import { and, eq, inArray, isNull, sql } from "drizzle-orm";
16
16
 
17
17
  import { getDb, schema } from "../db/index.js";
18
18
 
@@ -37,6 +37,7 @@ export type UploadLeaseResult =
37
37
  | { held: true }
38
38
  | {
39
39
  held: false;
40
+ staleAttempt: boolean;
40
41
  status: string | null;
41
42
  failureReason: string | null;
42
43
  videoUrl: string | null;
@@ -53,7 +54,12 @@ export type UploadLeaseResult =
53
54
  */
54
55
  export async function renewUploadLease(
55
56
  recordingId: string,
56
- options: { now?: number; uploadProgress?: number } = {},
57
+ options: {
58
+ now?: number;
59
+ uploadProgress?: number;
60
+ attemptId?: string | null;
61
+ generationId?: string | null;
62
+ } = {},
57
63
  ): Promise<UploadLeaseResult> {
58
64
  const now = options.now ?? Date.now();
59
65
  const held = await getDb()
@@ -72,6 +78,23 @@ export async function renewUploadLease(
72
78
  and(
73
79
  eq(schema.recordings.id, recordingId),
74
80
  inArray(schema.recordings.status, [...IN_PROGRESS_STATUSES]),
81
+ ...(options.attemptId === undefined
82
+ ? []
83
+ : [
84
+ options.attemptId === null
85
+ ? isNull(schema.recordings.uploadAttemptId)
86
+ : eq(schema.recordings.uploadAttemptId, options.attemptId),
87
+ ]),
88
+ ...(options.generationId === undefined
89
+ ? []
90
+ : [
91
+ options.generationId === null
92
+ ? isNull(schema.recordings.uploadGenerationId)
93
+ : eq(
94
+ schema.recordings.uploadGenerationId,
95
+ options.generationId,
96
+ ),
97
+ ]),
75
98
  ),
76
99
  )
77
100
  .returning({ id: schema.recordings.id });
@@ -87,12 +110,23 @@ export async function renewUploadLease(
87
110
  videoUrl: schema.recordings.videoUrl,
88
111
  videoSizeBytes: schema.recordings.videoSizeBytes,
89
112
  durationMs: schema.recordings.durationMs,
113
+ uploadAttemptId: schema.recordings.uploadAttemptId,
114
+ uploadGenerationId: schema.recordings.uploadGenerationId,
90
115
  })
91
116
  .from(schema.recordings)
92
117
  .where(eq(schema.recordings.id, recordingId));
93
118
 
94
119
  return {
95
120
  held: false,
121
+ staleAttempt:
122
+ (options.attemptId !== undefined || options.generationId !== undefined) &&
123
+ IN_PROGRESS_STATUSES.includes(
124
+ row?.status as (typeof IN_PROGRESS_STATUSES)[number],
125
+ ) &&
126
+ ((options.attemptId !== undefined &&
127
+ row?.uploadAttemptId !== options.attemptId) ||
128
+ (options.generationId !== undefined &&
129
+ row?.uploadGenerationId !== options.generationId)),
96
130
  status: row?.status ?? null,
97
131
  failureReason: row?.failureReason ?? null,
98
132
  videoUrl: row?.videoUrl ?? null,
@@ -106,6 +140,7 @@ export interface ReapedUpload {
106
140
  ownerEmail: string;
107
141
  status: string;
108
142
  leaseExpiresAt: string;
143
+ uploadGenerationId: string | null;
109
144
  }
110
145
 
111
146
  export interface ReapResult {
@@ -137,7 +172,7 @@ export async function reapExpiredUploads(
137
172
 
138
173
  // guard:allow-unscoped — system upload reaper, owner-agnostic by design.
139
174
  const probe = await exec.execute({
140
- sql: `SELECT id, owner_email, status, upload_lease_expires_at
175
+ sql: `SELECT id, owner_email, status, upload_lease_expires_at, upload_generation_id
141
176
  FROM recordings
142
177
  WHERE status IN ('uploading', 'processing')
143
178
  AND upload_lease_expires_at IS NOT NULL
@@ -154,6 +189,10 @@ export async function reapExpiredUploads(
154
189
  ownerEmail: String(row.owner_email ?? ""),
155
190
  status: String(row.status ?? ""),
156
191
  leaseExpiresAt: String(row.upload_lease_expires_at ?? ""),
192
+ uploadGenerationId:
193
+ typeof row.upload_generation_id === "string"
194
+ ? row.upload_generation_id
195
+ : null,
157
196
  }));
158
197
 
159
198
  let failed = 0;
@@ -184,9 +223,16 @@ export async function reapExpiredUploads(
184
223
  failed = terminated.size;
185
224
 
186
225
  for (const id of terminated) {
226
+ const generationId = expired.find(
227
+ (row) => row.id === id,
228
+ )?.uploadGenerationId;
187
229
  await exec.execute({
188
230
  sql: `DELETE FROM application_state WHERE key = ${p(1)}`,
189
- args: [`resumable-session-${id}`],
231
+ args: [
232
+ generationId
233
+ ? `resumable-session-${id}-${generationId}`
234
+ : `resumable-session-${id}`,
235
+ ],
190
236
  });
191
237
  }
192
238
  }
@@ -0,0 +1,207 @@
1
+ /**
2
+ * Generate a filmstrip sprite — one JPEG holding a grid of evenly-spaced video
3
+ * frames — so the editor timeline renders thumbnails as a single cached image
4
+ * request instead of decoding the video in the browser.
5
+ *
6
+ * Why a sprite rather than per-frame files: one ffmpeg pass, one upload, one
7
+ * HTTP request, and the grid geometry is enough for CSS `background-position`
8
+ * to address any cell. The browser fallback in `app/lib/video-filmstrip.ts`
9
+ * needs one seek per frame and cannot read cross-origin media at all.
10
+ *
11
+ * Three constraints worth keeping in mind:
12
+ *
13
+ * - Cells are padded to an exact size (`force_original_aspect_ratio=decrease`
14
+ * plus `pad`) so the returned grid geometry is exact without probing the
15
+ * source. Letterboxed bars on odd aspect ratios are the deliberate cost of
16
+ * not having to decode the output to learn its dimensions.
17
+ * - Sampling starts half a cell in, matching the browser fallback: cell `i`
18
+ * shows the midpoint of the time slot it occupies, not its leading edge.
19
+ * - Input arrives as bytes and is written to a temp file, like every other
20
+ * ffmpeg path here. That means the whole file is buffered in memory by the
21
+ * caller, which is the practical ceiling on this approach for long
22
+ * recordings on small hosts.
23
+ */
24
+
25
+ import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
26
+ import { tmpdir } from "node:os";
27
+ import { join } from "node:path";
28
+
29
+ import { isFfmpegAvailable, runFfmpeg, withRemuxSlot } from "./video-remux.js";
30
+
31
+ const SPRITE_TIMEOUT_MS = 90_000;
32
+
33
+ export const DEFAULT_FILMSTRIP_FRAME_COUNT = 40;
34
+ // A sprite is decoded whole by the browser, so cap the grid: 120 cells at the
35
+ // default size is already a ~1600x1080 image.
36
+ export const MAX_FILMSTRIP_FRAME_COUNT = 120;
37
+ export const DEFAULT_FILMSTRIP_FRAME_WIDTH = 160;
38
+ export const DEFAULT_FILMSTRIP_FRAME_HEIGHT = 90;
39
+ export const DEFAULT_FILMSTRIP_COLUMNS = 10;
40
+
41
+ export type FilmstripSpriteStatus =
42
+ | "generated"
43
+ | "skipped-no-ffmpeg"
44
+ | "skipped-no-duration"
45
+ | "skipped-no-media"
46
+ | "failed-ffmpeg"
47
+ | "failed-empty-output";
48
+
49
+ /** Everything the UI needs to address one cell of the sprite. */
50
+ export interface FilmstripSpriteGrid {
51
+ frameCount: number;
52
+ columns: number;
53
+ rows: number;
54
+ frameWidth: number;
55
+ frameHeight: number;
56
+ }
57
+
58
+ export interface FilmstripSpriteResult {
59
+ status: FilmstripSpriteStatus;
60
+ /** Present only when `status === "generated"`. */
61
+ sprite?: { bytes: Uint8Array; grid: FilmstripSpriteGrid };
62
+ detail?: string;
63
+ }
64
+
65
+ export interface GenerateFilmstripSpriteInput {
66
+ mediaBytes: Uint8Array;
67
+ durationMs: number;
68
+ frameCount?: number;
69
+ frameWidth?: number;
70
+ frameHeight?: number;
71
+ columns?: number;
72
+ }
73
+
74
+ /**
75
+ * Lay `frameCount` cells out into a grid no wider than `columns`. Exported so
76
+ * callers can compute the same geometry the sprite will have without
77
+ * regenerating it.
78
+ */
79
+ export function filmstripGrid(input: {
80
+ frameCount: number;
81
+ columns: number;
82
+ frameWidth: number;
83
+ frameHeight: number;
84
+ }): FilmstripSpriteGrid {
85
+ const frameCount = Math.min(
86
+ MAX_FILMSTRIP_FRAME_COUNT,
87
+ Math.max(1, Math.floor(input.frameCount)),
88
+ );
89
+ const columns = Math.max(1, Math.min(input.columns, frameCount));
90
+ return {
91
+ frameCount,
92
+ columns,
93
+ rows: Math.ceil(frameCount / columns),
94
+ frameWidth: input.frameWidth,
95
+ frameHeight: input.frameHeight,
96
+ };
97
+ }
98
+
99
+ /**
100
+ * Build the ffmpeg filter chain that samples `frameCount` frames across
101
+ * `durationMs` and tiles them into `grid`. Exported for tests: the sampling
102
+ * offset and rate are the parts most likely to drift from the browser
103
+ * fallback's expectations.
104
+ */
105
+ export function filmstripSpriteFilter(input: {
106
+ durationMs: number;
107
+ grid: FilmstripSpriteGrid;
108
+ }): { seekSeconds: string; filter: string } {
109
+ const { durationMs, grid } = input;
110
+ const cellMs = durationMs / grid.frameCount;
111
+ const fps = grid.frameCount / (durationMs / 1000);
112
+
113
+ return {
114
+ // Half a cell in, so each tile is the midpoint of its slot.
115
+ seekSeconds: (cellMs / 2 / 1000).toFixed(6),
116
+ filter: [
117
+ `fps=${fps.toFixed(6)}`,
118
+ `scale=${grid.frameWidth}:${grid.frameHeight}:force_original_aspect_ratio=decrease`,
119
+ `pad=${grid.frameWidth}:${grid.frameHeight}:-1:-1:color=black`,
120
+ `tile=${grid.columns}x${grid.rows}`,
121
+ ].join(","),
122
+ };
123
+ }
124
+
125
+ export async function generateFilmstripSprite(
126
+ input: GenerateFilmstripSpriteInput,
127
+ ): Promise<FilmstripSpriteResult> {
128
+ const {
129
+ mediaBytes,
130
+ durationMs,
131
+ frameCount = DEFAULT_FILMSTRIP_FRAME_COUNT,
132
+ frameWidth = DEFAULT_FILMSTRIP_FRAME_WIDTH,
133
+ frameHeight = DEFAULT_FILMSTRIP_FRAME_HEIGHT,
134
+ columns = DEFAULT_FILMSTRIP_COLUMNS,
135
+ } = input;
136
+
137
+ if (mediaBytes.byteLength === 0) {
138
+ return { status: "skipped-no-media" };
139
+ }
140
+ if (!Number.isFinite(durationMs) || durationMs <= 0) {
141
+ return { status: "skipped-no-duration" };
142
+ }
143
+ if (!isFfmpegAvailable()) {
144
+ return {
145
+ status: "skipped-no-ffmpeg",
146
+ detail:
147
+ "No ffmpeg binary available; the editor falls back to browser-side frame extraction.",
148
+ };
149
+ }
150
+
151
+ const grid = filmstripGrid({ frameCount, columns, frameWidth, frameHeight });
152
+ const { seekSeconds, filter } = filmstripSpriteFilter({ durationMs, grid });
153
+
154
+ const dir = await mkdtemp(join(tmpdir(), "clips-filmstrip-"));
155
+ const inputPath = join(dir, "input.media");
156
+ const outputPath = join(dir, "sprite.jpg");
157
+
158
+ try {
159
+ await writeFile(inputPath, mediaBytes);
160
+ await withRemuxSlot(() =>
161
+ runFfmpeg(
162
+ [
163
+ "-hide_banner",
164
+ "-loglevel",
165
+ "error",
166
+ "-nostdin",
167
+ "-y",
168
+ // Input seeking: cheap, and puts the first sample at a cell midpoint.
169
+ "-ss",
170
+ seekSeconds,
171
+ "-i",
172
+ inputPath,
173
+ "-an",
174
+ "-sn",
175
+ "-vf",
176
+ filter,
177
+ // The tile filter emits the finished grid as a single frame.
178
+ "-frames:v",
179
+ "1",
180
+ "-q:v",
181
+ "4",
182
+ "-f",
183
+ "image2",
184
+ outputPath,
185
+ ],
186
+ { timeoutMs: SPRITE_TIMEOUT_MS, label: "filmstrip sprite" },
187
+ ),
188
+ );
189
+
190
+ const bytes = new Uint8Array(await readFile(outputPath));
191
+ if (bytes.byteLength === 0) {
192
+ return {
193
+ status: "failed-empty-output",
194
+ detail: "ffmpeg reported success but wrote an empty sprite",
195
+ };
196
+ }
197
+
198
+ return { status: "generated", sprite: { bytes, grid } };
199
+ } catch (err) {
200
+ return {
201
+ status: "failed-ffmpeg",
202
+ detail: err instanceof Error ? err.message : String(err),
203
+ };
204
+ } finally {
205
+ await rm(dir, { recursive: true, force: true }).catch(() => {});
206
+ }
207
+ }
@@ -125,7 +125,11 @@ function startsWithMagic(bytes: Uint8Array, magic: number[]): boolean {
125
125
  return true;
126
126
  }
127
127
 
128
- async function runFfmpeg(args: string[]): Promise<void> {
128
+ export async function runFfmpeg(
129
+ args: string[],
130
+ options: { timeoutMs?: number; label?: string } = {},
131
+ ): Promise<void> {
132
+ const { timeoutMs = REMUX_TIMEOUT_MS, label = "remux" } = options;
129
133
  await new Promise<void>((resolve, reject) => {
130
134
  const child = spawn(ffmpegCommand(), args, {
131
135
  stdio: ["ignore", "ignore", "pipe"],
@@ -133,8 +137,8 @@ async function runFfmpeg(args: string[]): Promise<void> {
133
137
  let stderr = "";
134
138
  const timeout = setTimeout(() => {
135
139
  child.kill("SIGKILL");
136
- reject(new Error(`ffmpeg remux timed out\n${stderr}`));
137
- }, REMUX_TIMEOUT_MS);
140
+ reject(new Error(`ffmpeg ${label} timed out\n${stderr}`));
141
+ }, timeoutMs);
138
142
 
139
143
  child.stderr?.on("data", (chunk: Buffer) => {
140
144
  stderr = (stderr + chunk.toString("utf8")).slice(-STDERR_LIMIT);
@@ -244,7 +248,12 @@ export async function probeHasAudioStream(
244
248
  }
245
249
  }
246
250
 
247
- async function withRemuxSlot<T>(fn: () => Promise<T>): Promise<T> {
251
+ /**
252
+ * Bound concurrent ffmpeg invocations process-wide. Every ffmpeg caller must go
253
+ * through this, including non-remux jobs — the limit protects the box, not the
254
+ * remux path specifically.
255
+ */
256
+ export async function withRemuxSlot<T>(fn: () => Promise<T>): Promise<T> {
248
257
  if (activeRemuxes >= MAX_CONCURRENT_REMUXES) {
249
258
  await new Promise<void>((resolve) => remuxWaiters.push(resolve));
250
259
  }