@agent-native/core 0.134.1 → 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 (442) hide show
  1. package/corpus/README.md +3 -3
  2. package/corpus/core/CHANGELOG.md +107 -0
  3. package/corpus/core/package.json +3 -2
  4. package/corpus/core/src/automations/service.ts +41 -4
  5. package/corpus/core/src/cli/mcp-config-writers.ts +27 -21
  6. package/corpus/core/src/client/agent-page/AgentJobsTab.tsx +187 -85
  7. package/corpus/core/src/client/agent-page/AutomationDetailsDialog.tsx +208 -0
  8. package/corpus/core/src/client/agent-page/AutomationScheduleDialog.tsx +180 -0
  9. package/corpus/core/src/client/agent-page/TimezoneSelect.tsx +241 -0
  10. package/corpus/core/src/client/agent-page/use-jobs.ts +50 -10
  11. package/corpus/core/src/client/org/OrgSwitcher.tsx +26 -0
  12. package/corpus/core/src/client/settings/AccountSettingsCard.tsx +35 -31
  13. package/corpus/core/src/client/settings/SchedulingTimezoneField.tsx +71 -0
  14. package/corpus/core/src/client/settings/SettingsRow.tsx +113 -0
  15. package/corpus/core/src/client/settings/agent-settings-search.ts +2 -1
  16. package/corpus/core/src/client/settings/index.ts +6 -0
  17. package/corpus/core/src/integrations/identity.ts +2 -12
  18. package/corpus/core/src/jobs/actions/list-automation-runs.ts +33 -0
  19. package/corpus/core/src/jobs/actions/list-recurring-jobs.ts +27 -6
  20. package/corpus/core/src/jobs/actions/manage-recurring-job.ts +41 -9
  21. package/corpus/core/src/jobs/background-automation-runner.ts +88 -0
  22. package/corpus/core/src/jobs/cron.ts +52 -6
  23. package/corpus/core/src/jobs/frontmatter.ts +20 -0
  24. package/corpus/core/src/jobs/run-history.ts +242 -0
  25. package/corpus/core/src/jobs/scheduler.ts +73 -18
  26. package/corpus/core/src/jobs/tools.ts +54 -6
  27. package/corpus/core/src/localization/actions/get-localization-preference.ts +2 -2
  28. package/corpus/core/src/localization/actions/set-localization-preference.ts +49 -11
  29. package/corpus/core/src/localization/default-messages.ts +27 -0
  30. package/corpus/core/src/localization/shared.ts +39 -6
  31. package/corpus/core/src/localization/user-timezone.ts +36 -0
  32. package/corpus/core/src/org/app-roles-handlers.ts +1 -9
  33. package/corpus/core/src/org/index.ts +2 -0
  34. package/corpus/core/src/org/membership.ts +18 -0
  35. package/corpus/core/src/review/actions/create-review-comment.ts +4 -1
  36. package/corpus/core/src/review/actions/reply-review-comment.ts +4 -1
  37. package/corpus/core/src/review/index.ts +5 -0
  38. package/corpus/core/src/review/notifications.ts +163 -0
  39. package/corpus/core/src/review/types.ts +7 -0
  40. package/corpus/core/src/server/action-discovery.ts +4 -0
  41. package/corpus/core/src/server/activity-notifications.ts +174 -0
  42. package/corpus/core/src/server/app-name.ts +31 -0
  43. package/corpus/core/src/server/better-auth-instance.ts +4 -4
  44. package/corpus/core/src/server/email-templates.ts +70 -14
  45. package/corpus/core/src/server/email.ts +66 -3
  46. package/corpus/core/src/server/index.ts +10 -0
  47. package/corpus/core/src/sharing/index.ts +5 -0
  48. package/corpus/core/src/sharing/recipients.ts +67 -0
  49. package/corpus/core/src/triggers/actions/list-automations.ts +27 -8
  50. package/corpus/core/src/triggers/actions/manage-automation.ts +17 -6
  51. package/corpus/core/src/triggers/actions.ts +16 -4
  52. package/corpus/core/src/triggers/dispatcher.ts +44 -30
  53. package/corpus/core/src/triggers/routes.ts +19 -6
  54. package/corpus/core/src/vitest-config.ts +68 -0
  55. package/corpus/core/vitest.config.ts +5 -0
  56. package/corpus/templates/analytics/app/pages/Settings.tsx +37 -53
  57. package/corpus/templates/analytics/changelog/2026-08-01-new-error-alerts-now-reach-you-by-email-and-a-scheduled-dash.md +6 -0
  58. package/corpus/templates/analytics/server/jobs/dashboard-report.ts +43 -0
  59. package/corpus/templates/analytics/server/lib/error-capture.ts +5 -1
  60. package/corpus/templates/analytics/vitest.config.ts +16 -12
  61. package/corpus/templates/assets/actions/refresh-generation-run.ts +3 -0
  62. package/corpus/templates/assets/app/components/ui/switch.tsx +1 -0
  63. package/corpus/templates/assets/app/hooks/use-assets-prefs.ts +55 -0
  64. package/corpus/templates/assets/app/i18n-data.ts +30 -0
  65. package/corpus/templates/assets/app/routes/settings.tsx +58 -55
  66. package/corpus/templates/assets/changelog/2026-08-01-get-an-email-when-a-generation-you-started-finishes-or-fails.md +6 -0
  67. package/corpus/templates/assets/server/lib/generation-run-notifications.ts +89 -0
  68. package/corpus/templates/assets/server/lib/video-runs.ts +8 -1
  69. package/corpus/templates/assets/server/routes/_agent-native/assets/user-prefs.get.ts +17 -0
  70. package/corpus/templates/assets/server/routes/_agent-native/assets/user-prefs.put.ts +36 -0
  71. package/corpus/templates/assets/shared/assets-user-prefs.ts +10 -0
  72. package/corpus/templates/assets/vitest.config.ts +16 -12
  73. package/corpus/templates/brain/app/routes/settings.tsx +15 -15
  74. package/corpus/templates/brain/vitest.config.ts +16 -12
  75. package/corpus/templates/calendar/app/pages/Settings.tsx +32 -40
  76. package/corpus/templates/calendar/changelog/2026-08-01-language-and-appearance-now-sit-together-in-one-preferences-.md +6 -0
  77. package/corpus/templates/calendar/vitest.config.ts +16 -12
  78. package/corpus/templates/chat/app/routes/settings.tsx +14 -22
  79. package/corpus/templates/chat/vitest.config.ts +9 -0
  80. package/corpus/templates/clips/.agents/skills/video-editing/SKILL.md +31 -0
  81. package/corpus/templates/clips/AGENTS.md +1 -0
  82. package/corpus/templates/clips/actions/add-comment.ts +12 -1
  83. package/corpus/templates/clips/actions/create-meeting.ts +2 -2
  84. package/corpus/templates/clips/actions/create-recording.ts +2 -2
  85. package/corpus/templates/clips/actions/finalize-recording.ts +42 -6
  86. package/corpus/templates/clips/actions/generate-filmstrip.ts +154 -0
  87. package/corpus/templates/clips/actions/get-recording-player-data.ts +6 -0
  88. package/corpus/templates/clips/actions/import-loom-recording.ts +2 -2
  89. package/corpus/templates/clips/actions/lib/ensure-recording-filmstrip.ts +258 -0
  90. package/corpus/templates/clips/actions/lib/ensure-seekable-video.ts +11 -3
  91. package/corpus/templates/clips/actions/react-to-comment.ts +13 -0
  92. package/corpus/templates/clips/actions/react-to-recording.ts +10 -1
  93. package/corpus/templates/clips/actions/reply-to-comment.ts +12 -1
  94. package/corpus/templates/clips/actions/stitch-recordings.ts +2 -2
  95. package/corpus/templates/clips/app/components/editor/editor-layout.tsx +152 -23
  96. package/corpus/templates/clips/app/components/editor/waveform.tsx +237 -75
  97. package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +23 -1
  98. package/corpus/templates/clips/app/components/settings/ai-setup-section.tsx +441 -0
  99. package/corpus/templates/clips/app/components/settings/slack-section.tsx +329 -0
  100. package/corpus/templates/clips/app/components/settings/types.ts +14 -0
  101. package/corpus/templates/clips/app/components/settings/video-storage-section.tsx +443 -0
  102. package/corpus/templates/clips/app/hooks/use-secret-status.ts +69 -0
  103. package/corpus/templates/clips/app/i18n/en-US.ts +9 -1
  104. package/corpus/templates/clips/app/lib/video-filmstrip.ts +262 -0
  105. package/corpus/templates/clips/app/routes/_app.settings._index.tsx +165 -1351
  106. package/corpus/templates/clips/changelog/2026-07-27-saved-clips-now-resume-from-the-last-uploaded-byte-after-an-.md +6 -0
  107. package/corpus/templates/clips/changelog/2026-07-31-editor-timeline-now-shows-video-frames-behind-the-waveform-g.md +6 -0
  108. package/corpus/templates/clips/changelog/2026-07-31-email-notifications-now-go-out-when-someone-comments-on-or-r.md +6 -0
  109. package/corpus/templates/clips/changelog/2026-07-31-you-can-now-set-a-default-visibility-for-new-recordings-in-s.md +6 -0
  110. package/corpus/templates/clips/changelog/2026-08-01-email-notifications-for-comments-and-reactions-now-actually-.md +6 -0
  111. package/corpus/templates/clips/changelog/2026-08-03-the-desktop-app-now-opens-directly-beneath-its-tray-icon-on-.md +6 -0
  112. package/corpus/templates/clips/chrome-extension/package.json +1 -0
  113. package/corpus/templates/clips/chrome-extension/vitest.config.ts +9 -0
  114. package/corpus/templates/clips/desktop/package.json +1 -0
  115. package/corpus/templates/clips/desktop/src/app.tsx +17 -1
  116. package/corpus/templates/clips/desktop/src/lib/recorder.ts +331 -36
  117. package/corpus/templates/clips/desktop/src/lib/upload-recovery.ts +129 -0
  118. package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +815 -15
  119. package/corpus/templates/clips/desktop/src-tauri/src/tray.rs +61 -11
  120. package/corpus/templates/clips/desktop/vitest.config.ts +9 -0
  121. package/corpus/templates/clips/server/db/schema.ts +17 -0
  122. package/corpus/templates/clips/server/jobs/media-verification.ts +4 -0
  123. package/corpus/templates/clips/server/lib/activity-notifications.ts +185 -0
  124. package/corpus/templates/clips/server/lib/recording-media-cleanup.ts +2 -0
  125. package/corpus/templates/clips/server/lib/recording-upload-state.ts +46 -9
  126. package/corpus/templates/clips/server/lib/recordings.ts +24 -0
  127. package/corpus/templates/clips/server/lib/resumable-session.ts +10 -4
  128. package/corpus/templates/clips/server/lib/transactional-email-templates.ts +109 -0
  129. package/corpus/templates/clips/server/lib/upload-interruption.ts +8 -0
  130. package/corpus/templates/clips/server/lib/upload-lease.ts +50 -4
  131. package/corpus/templates/clips/server/lib/video-filmstrip-sprite.ts +207 -0
  132. package/corpus/templates/clips/server/lib/video-remux.ts +13 -4
  133. package/corpus/templates/clips/server/plugins/db.ts +16 -0
  134. package/corpus/templates/clips/server/routes/api/_agent-native-background/post-finalize-worker.post.ts +4 -0
  135. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/abort.post.ts +123 -36
  136. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/chunk.post.ts +159 -29
  137. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/interrupt.post.ts +197 -0
  138. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/reset-chunks.post.ts +193 -39
  139. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/resume.get.ts +191 -12
  140. package/corpus/templates/clips/shared/clips-ai-prefs.ts +6 -0
  141. package/corpus/templates/clips/shared/feature-flags.ts +12 -0
  142. package/corpus/templates/clips/shared/recording-core.ts +4 -0
  143. package/corpus/templates/clips/vitest.config.ts +23 -19
  144. package/corpus/templates/content/actions/add-comment.ts +15 -1
  145. package/corpus/templates/content/actions/list-documents.ts +1 -0
  146. package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +5 -2
  147. package/corpus/templates/content/app/components/editor/DocumentToolbar.tsx +50 -1
  148. package/corpus/templates/content/app/components/ui/switch.tsx +1 -0
  149. package/corpus/templates/content/app/hooks/use-content-prefs.ts +55 -0
  150. package/corpus/templates/content/app/i18n-data.ts +40 -0
  151. package/corpus/templates/content/app/routes/_app.settings.tsx +47 -18
  152. package/corpus/templates/content/changelog/2026-07-30-breadcrumb-menus-now-hide-internal-system-pages.md +6 -0
  153. package/corpus/templates/content/changelog/2026-08-01-get-an-email-when-someone-comments-on-replies-in-or-mentions.md +6 -0
  154. package/corpus/templates/content/server/lib/comment-notifications.ts +146 -0
  155. package/corpus/templates/content/server/routes/_agent-native/content/user-prefs.get.ts +17 -0
  156. package/corpus/templates/content/server/routes/_agent-native/content/user-prefs.put.ts +36 -0
  157. package/corpus/templates/content/shared/content-user-prefs.ts +11 -0
  158. package/corpus/templates/content/vitest.config.ts +20 -16
  159. package/corpus/templates/crm/app/routes/settings.tsx +14 -22
  160. package/corpus/templates/crm/vitest.config.ts +8 -4
  161. package/corpus/templates/design/app/routes/settings.tsx +14 -22
  162. package/corpus/templates/design/changelog/2026-08-01-review-comments-replies-and-mentions-now-send-an-email-to-th.md +6 -0
  163. package/corpus/templates/design/vitest.config.ts +18 -12
  164. package/corpus/templates/dispatch/app/routes/settings.tsx +26 -40
  165. package/corpus/templates/dispatch/vitest.config.ts +11 -7
  166. package/corpus/templates/factory/app/routes/settings.tsx +14 -22
  167. package/corpus/templates/factory/vitest.config.ts +9 -0
  168. package/corpus/templates/forms/app/routes/_app.settings.tsx +14 -22
  169. package/corpus/templates/forms/vitest.config.ts +27 -23
  170. package/corpus/templates/macros/app/routes/settings.tsx +14 -22
  171. package/corpus/templates/macros/vitest.config.ts +16 -12
  172. package/corpus/templates/mail/app/pages/SettingsPage.tsx +14 -16
  173. package/corpus/templates/mail/vitest.config.ts +16 -12
  174. package/corpus/templates/plan/app/routes/settings.tsx +30 -44
  175. package/corpus/templates/plan/vitest.config.ts +24 -20
  176. package/corpus/templates/slides/actions/add-slide-comment.ts +12 -1
  177. package/corpus/templates/slides/app/components/ui/switch.tsx +1 -0
  178. package/corpus/templates/slides/app/hooks/use-slides-prefs.ts +55 -0
  179. package/corpus/templates/slides/app/i18n/en-US.ts +4 -0
  180. package/corpus/templates/slides/app/routes/settings.tsx +45 -22
  181. package/corpus/templates/slides/changelog/2026-08-01-get-an-email-when-someone-comments-on-or-replies-in-your-dec.md +6 -0
  182. package/corpus/templates/slides/server/lib/comment-notifications.ts +182 -0
  183. package/corpus/templates/slides/server/routes/_agent-native/slides/user-prefs.get.ts +16 -0
  184. package/corpus/templates/slides/server/routes/_agent-native/slides/user-prefs.put.ts +36 -0
  185. package/corpus/templates/slides/shared/slides-user-prefs.ts +11 -0
  186. package/corpus/templates/slides/vitest.config.ts +17 -13
  187. package/corpus/templates/tasks/app/components/layout/Sidebar.tsx +2 -0
  188. package/corpus/templates/tasks/app/routes/settings.tsx +14 -22
  189. package/corpus/templates/tasks/vitest.config.ts +17 -13
  190. package/corpus/toolkit/CHANGELOG.md +9 -0
  191. package/corpus/toolkit/package.json +1 -1
  192. package/corpus/toolkit/vitest.config.ts +3 -0
  193. package/dist/automations/service.d.ts +2 -0
  194. package/dist/automations/service.d.ts.map +1 -1
  195. package/dist/automations/service.js +29 -4
  196. package/dist/automations/service.js.map +1 -1
  197. package/dist/cli/mcp-config-writers.d.ts.map +1 -1
  198. package/dist/cli/mcp-config-writers.js +24 -23
  199. package/dist/cli/mcp-config-writers.js.map +1 -1
  200. package/dist/client/agent-page/AgentJobsTab.d.ts.map +1 -1
  201. package/dist/client/agent-page/AgentJobsTab.js +86 -29
  202. package/dist/client/agent-page/AgentJobsTab.js.map +1 -1
  203. package/dist/client/agent-page/AutomationDetailsDialog.d.ts +21 -0
  204. package/dist/client/agent-page/AutomationDetailsDialog.d.ts.map +1 -0
  205. package/dist/client/agent-page/AutomationDetailsDialog.js +33 -0
  206. package/dist/client/agent-page/AutomationDetailsDialog.js.map +1 -0
  207. package/dist/client/agent-page/AutomationScheduleDialog.d.ts +15 -0
  208. package/dist/client/agent-page/AutomationScheduleDialog.d.ts.map +1 -0
  209. package/dist/client/agent-page/AutomationScheduleDialog.js +46 -0
  210. package/dist/client/agent-page/AutomationScheduleDialog.js.map +1 -0
  211. package/dist/client/agent-page/TimezoneSelect.d.ts +17 -0
  212. package/dist/client/agent-page/TimezoneSelect.d.ts.map +1 -0
  213. package/dist/client/agent-page/TimezoneSelect.js +107 -0
  214. package/dist/client/agent-page/TimezoneSelect.js.map +1 -0
  215. package/dist/client/agent-page/use-jobs.d.ts +20 -7
  216. package/dist/client/agent-page/use-jobs.d.ts.map +1 -1
  217. package/dist/client/agent-page/use-jobs.js +24 -4
  218. package/dist/client/agent-page/use-jobs.js.map +1 -1
  219. package/dist/client/org/OrgSwitcher.d.ts +8 -1
  220. package/dist/client/org/OrgSwitcher.d.ts.map +1 -1
  221. package/dist/client/org/OrgSwitcher.js +8 -2
  222. package/dist/client/org/OrgSwitcher.js.map +1 -1
  223. package/dist/client/settings/AccountSettingsCard.d.ts.map +1 -1
  224. package/dist/client/settings/AccountSettingsCard.js +10 -9
  225. package/dist/client/settings/AccountSettingsCard.js.map +1 -1
  226. package/dist/client/settings/SchedulingTimezoneField.d.ts +10 -0
  227. package/dist/client/settings/SchedulingTimezoneField.d.ts.map +1 -0
  228. package/dist/client/settings/SchedulingTimezoneField.js +30 -0
  229. package/dist/client/settings/SchedulingTimezoneField.js.map +1 -0
  230. package/dist/client/settings/SettingsRow.d.ts +36 -0
  231. package/dist/client/settings/SettingsRow.d.ts.map +1 -0
  232. package/dist/client/settings/SettingsRow.js +15 -0
  233. package/dist/client/settings/SettingsRow.js.map +1 -0
  234. package/dist/client/settings/agent-settings-search.d.ts.map +1 -1
  235. package/dist/client/settings/agent-settings-search.js +1 -1
  236. package/dist/client/settings/agent-settings-search.js.map +1 -1
  237. package/dist/client/settings/index.d.ts +1 -0
  238. package/dist/client/settings/index.d.ts.map +1 -1
  239. package/dist/client/settings/index.js +1 -0
  240. package/dist/client/settings/index.js.map +1 -1
  241. package/dist/collab/struct-routes.d.ts +1 -1
  242. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  243. package/dist/integrations/identity.d.ts.map +1 -1
  244. package/dist/integrations/identity.js +2 -11
  245. package/dist/integrations/identity.js.map +1 -1
  246. package/dist/jobs/actions/list-automation-runs.d.ts +8 -0
  247. package/dist/jobs/actions/list-automation-runs.d.ts.map +1 -0
  248. package/dist/jobs/actions/list-automation-runs.js +29 -0
  249. package/dist/jobs/actions/list-automation-runs.js.map +1 -0
  250. package/dist/jobs/actions/list-recurring-jobs.d.ts +2 -0
  251. package/dist/jobs/actions/list-recurring-jobs.d.ts.map +1 -1
  252. package/dist/jobs/actions/list-recurring-jobs.js +20 -7
  253. package/dist/jobs/actions/list-recurring-jobs.js.map +1 -1
  254. package/dist/jobs/actions/manage-recurring-job.d.ts +2 -0
  255. package/dist/jobs/actions/manage-recurring-job.js +30 -9
  256. package/dist/jobs/actions/manage-recurring-job.js.map +1 -1
  257. package/dist/jobs/background-automation-runner.d.ts.map +1 -1
  258. package/dist/jobs/background-automation-runner.js +60 -0
  259. package/dist/jobs/background-automation-runner.js.map +1 -1
  260. package/dist/jobs/cron.d.ts +16 -2
  261. package/dist/jobs/cron.d.ts.map +1 -1
  262. package/dist/jobs/cron.js +43 -6
  263. package/dist/jobs/cron.js.map +1 -1
  264. package/dist/jobs/frontmatter.d.ts +12 -0
  265. package/dist/jobs/frontmatter.d.ts.map +1 -1
  266. package/dist/jobs/frontmatter.js +8 -0
  267. package/dist/jobs/frontmatter.js.map +1 -1
  268. package/dist/jobs/run-history.d.ts +56 -0
  269. package/dist/jobs/run-history.d.ts.map +1 -0
  270. package/dist/jobs/run-history.js +169 -0
  271. package/dist/jobs/run-history.js.map +1 -0
  272. package/dist/jobs/scheduler.d.ts.map +1 -1
  273. package/dist/jobs/scheduler.js +51 -18
  274. package/dist/jobs/scheduler.js.map +1 -1
  275. package/dist/jobs/tools.d.ts.map +1 -1
  276. package/dist/jobs/tools.js +37 -6
  277. package/dist/jobs/tools.js.map +1 -1
  278. package/dist/localization/actions/get-localization-preference.d.ts +1 -2
  279. package/dist/localization/actions/get-localization-preference.d.ts.map +1 -1
  280. package/dist/localization/actions/get-localization-preference.js.map +1 -1
  281. package/dist/localization/actions/set-localization-preference.d.ts +3 -3
  282. package/dist/localization/actions/set-localization-preference.d.ts.map +1 -1
  283. package/dist/localization/actions/set-localization-preference.js +33 -8
  284. package/dist/localization/actions/set-localization-preference.js.map +1 -1
  285. package/dist/localization/default-messages.d.ts +25 -0
  286. package/dist/localization/default-messages.d.ts.map +1 -1
  287. package/dist/localization/default-messages.js +25 -0
  288. package/dist/localization/default-messages.js.map +1 -1
  289. package/dist/localization/shared.d.ts +16 -1
  290. package/dist/localization/shared.d.ts.map +1 -1
  291. package/dist/localization/shared.js +22 -4
  292. package/dist/localization/shared.js.map +1 -1
  293. package/dist/localization/user-timezone.d.ts +11 -0
  294. package/dist/localization/user-timezone.d.ts.map +1 -0
  295. package/dist/localization/user-timezone.js +31 -0
  296. package/dist/localization/user-timezone.js.map +1 -0
  297. package/dist/notifications/routes.d.ts +1 -1
  298. package/dist/observability/routes.d.ts +1 -1
  299. package/dist/org/app-roles-handlers.d.ts.map +1 -1
  300. package/dist/org/app-roles-handlers.js +1 -8
  301. package/dist/org/app-roles-handlers.js.map +1 -1
  302. package/dist/org/index.d.ts +1 -0
  303. package/dist/org/index.d.ts.map +1 -1
  304. package/dist/org/index.js +1 -0
  305. package/dist/org/index.js.map +1 -1
  306. package/dist/org/membership.d.ts +6 -0
  307. package/dist/org/membership.d.ts.map +1 -0
  308. package/dist/org/membership.js +16 -0
  309. package/dist/org/membership.js.map +1 -0
  310. package/dist/provider-api/actions/custom-provider-registration.d.ts +10 -10
  311. package/dist/provider-api/actions/provider-api.d.ts +8 -8
  312. package/dist/provider-api/corpus-jobs.d.ts +2 -2
  313. package/dist/resources/handlers.d.ts +1 -1
  314. package/dist/review/actions/create-review-comment.d.ts +32 -1
  315. package/dist/review/actions/create-review-comment.d.ts.map +1 -1
  316. package/dist/review/actions/create-review-comment.js +3 -1
  317. package/dist/review/actions/create-review-comment.js.map +1 -1
  318. package/dist/review/actions/reply-review-comment.d.ts +32 -1
  319. package/dist/review/actions/reply-review-comment.d.ts.map +1 -1
  320. package/dist/review/actions/reply-review-comment.js +3 -1
  321. package/dist/review/actions/reply-review-comment.js.map +1 -1
  322. package/dist/review/index.d.ts +1 -0
  323. package/dist/review/index.d.ts.map +1 -1
  324. package/dist/review/index.js +1 -0
  325. package/dist/review/index.js.map +1 -1
  326. package/dist/review/notifications.d.ts +24 -0
  327. package/dist/review/notifications.d.ts.map +1 -0
  328. package/dist/review/notifications.js +123 -0
  329. package/dist/review/notifications.js.map +1 -0
  330. package/dist/review/types.d.ts +5 -0
  331. package/dist/review/types.d.ts.map +1 -1
  332. package/dist/review/types.js.map +1 -1
  333. package/dist/server/action-discovery.d.ts.map +1 -1
  334. package/dist/server/action-discovery.js +4 -0
  335. package/dist/server/action-discovery.js.map +1 -1
  336. package/dist/server/activity-notifications.d.ts +61 -0
  337. package/dist/server/activity-notifications.d.ts.map +1 -0
  338. package/dist/server/activity-notifications.js +103 -0
  339. package/dist/server/activity-notifications.js.map +1 -0
  340. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  341. package/dist/server/app-name.d.ts +18 -0
  342. package/dist/server/app-name.d.ts.map +1 -1
  343. package/dist/server/app-name.js +31 -0
  344. package/dist/server/app-name.js.map +1 -1
  345. package/dist/server/better-auth-instance.js +4 -4
  346. package/dist/server/better-auth-instance.js.map +1 -1
  347. package/dist/server/email-templates.d.ts +12 -0
  348. package/dist/server/email-templates.d.ts.map +1 -1
  349. package/dist/server/email-templates.js +57 -14
  350. package/dist/server/email-templates.js.map +1 -1
  351. package/dist/server/email.d.ts +12 -0
  352. package/dist/server/email.d.ts.map +1 -1
  353. package/dist/server/email.js +52 -5
  354. package/dist/server/email.js.map +1 -1
  355. package/dist/server/index.d.ts +1 -0
  356. package/dist/server/index.d.ts.map +1 -1
  357. package/dist/server/index.js +1 -0
  358. package/dist/server/index.js.map +1 -1
  359. package/dist/sharing/index.d.ts +1 -0
  360. package/dist/sharing/index.d.ts.map +1 -1
  361. package/dist/sharing/index.js +1 -0
  362. package/dist/sharing/index.js.map +1 -1
  363. package/dist/sharing/recipients.d.ts +31 -0
  364. package/dist/sharing/recipients.d.ts.map +1 -0
  365. package/dist/sharing/recipients.js +40 -0
  366. package/dist/sharing/recipients.js.map +1 -0
  367. package/dist/templates/chat/app/routes/settings.tsx +14 -22
  368. package/dist/templates/chat/vitest.config.ts +9 -0
  369. package/dist/triggers/actions/list-automations.d.ts +2 -0
  370. package/dist/triggers/actions/list-automations.d.ts.map +1 -1
  371. package/dist/triggers/actions/list-automations.js +19 -8
  372. package/dist/triggers/actions/list-automations.js.map +1 -1
  373. package/dist/triggers/actions/manage-automation.d.ts +6 -0
  374. package/dist/triggers/actions/manage-automation.js +12 -6
  375. package/dist/triggers/actions/manage-automation.js.map +1 -1
  376. package/dist/triggers/actions.d.ts.map +1 -1
  377. package/dist/triggers/actions.js +15 -4
  378. package/dist/triggers/actions.js.map +1 -1
  379. package/dist/triggers/dispatcher.d.ts.map +1 -1
  380. package/dist/triggers/dispatcher.js +19 -17
  381. package/dist/triggers/dispatcher.js.map +1 -1
  382. package/dist/triggers/routes.d.ts.map +1 -1
  383. package/dist/triggers/routes.js +6 -6
  384. package/dist/triggers/routes.js.map +1 -1
  385. package/dist/vitest-config.d.ts +10 -0
  386. package/dist/vitest-config.d.ts.map +1 -0
  387. package/dist/vitest-config.js +55 -0
  388. package/dist/vitest-config.js.map +1 -0
  389. package/package.json +5 -4
  390. package/src/automations/service.ts +41 -4
  391. package/src/cli/mcp-config-writers.ts +27 -21
  392. package/src/client/agent-page/AgentJobsTab.tsx +187 -85
  393. package/src/client/agent-page/AutomationDetailsDialog.tsx +208 -0
  394. package/src/client/agent-page/AutomationScheduleDialog.tsx +180 -0
  395. package/src/client/agent-page/TimezoneSelect.tsx +241 -0
  396. package/src/client/agent-page/use-jobs.ts +50 -10
  397. package/src/client/org/OrgSwitcher.tsx +26 -0
  398. package/src/client/settings/AccountSettingsCard.tsx +35 -31
  399. package/src/client/settings/SchedulingTimezoneField.tsx +71 -0
  400. package/src/client/settings/SettingsRow.tsx +113 -0
  401. package/src/client/settings/agent-settings-search.ts +2 -1
  402. package/src/client/settings/index.ts +6 -0
  403. package/src/integrations/identity.ts +2 -12
  404. package/src/jobs/actions/list-automation-runs.ts +33 -0
  405. package/src/jobs/actions/list-recurring-jobs.ts +27 -6
  406. package/src/jobs/actions/manage-recurring-job.ts +41 -9
  407. package/src/jobs/background-automation-runner.ts +88 -0
  408. package/src/jobs/cron.ts +52 -6
  409. package/src/jobs/frontmatter.ts +20 -0
  410. package/src/jobs/run-history.ts +242 -0
  411. package/src/jobs/scheduler.ts +73 -18
  412. package/src/jobs/tools.ts +54 -6
  413. package/src/localization/actions/get-localization-preference.ts +2 -2
  414. package/src/localization/actions/set-localization-preference.ts +49 -11
  415. package/src/localization/default-messages.ts +27 -0
  416. package/src/localization/shared.ts +39 -6
  417. package/src/localization/user-timezone.ts +36 -0
  418. package/src/org/app-roles-handlers.ts +1 -9
  419. package/src/org/index.ts +2 -0
  420. package/src/org/membership.ts +18 -0
  421. package/src/review/actions/create-review-comment.ts +4 -1
  422. package/src/review/actions/reply-review-comment.ts +4 -1
  423. package/src/review/index.ts +5 -0
  424. package/src/review/notifications.ts +163 -0
  425. package/src/review/types.ts +7 -0
  426. package/src/server/action-discovery.ts +4 -0
  427. package/src/server/activity-notifications.ts +174 -0
  428. package/src/server/app-name.ts +31 -0
  429. package/src/server/better-auth-instance.ts +4 -4
  430. package/src/server/email-templates.ts +70 -14
  431. package/src/server/email.ts +66 -3
  432. package/src/server/index.ts +10 -0
  433. package/src/sharing/index.ts +5 -0
  434. package/src/sharing/recipients.ts +67 -0
  435. package/src/templates/chat/app/routes/settings.tsx +14 -22
  436. package/src/templates/chat/vitest.config.ts +9 -0
  437. package/src/triggers/actions/list-automations.ts +27 -8
  438. package/src/triggers/actions/manage-automation.ts +17 -6
  439. package/src/triggers/actions.ts +16 -4
  440. package/src/triggers/dispatcher.ts +44 -30
  441. package/src/triggers/routes.ts +19 -6
  442. package/src/vitest-config.ts +68 -0
@@ -88,6 +88,13 @@ import {
88
88
  type CapturedTranscript,
89
89
  type TranscriptionCapture,
90
90
  } from "./transcription-capture";
91
+ import {
92
+ buildStreamingReplayPlan,
93
+ planStreamingRecovery,
94
+ retryAttemptIdAfterRestartSignal,
95
+ retryAttemptIdAfterResumeResponse,
96
+ type UploadResumeResponse,
97
+ } from "./upload-recovery";
91
98
  import {
92
99
  parseFinalizeReceipt,
93
100
  verifyFinalizeReceipt,
@@ -310,6 +317,7 @@ export interface PendingBrowserRecordingUpload {
310
317
  retryCount: number;
311
318
  chunkCount: number;
312
319
  mimeType: string;
320
+ uploadAttemptId?: string | null;
313
321
  }
314
322
 
315
323
  function streamFromTracks(tracks: MediaStreamTrack[]): MediaStream {
@@ -983,6 +991,16 @@ function buildRetryHeaders(mimeType: string, authToken?: string): Headers {
983
991
  return headers;
984
992
  }
985
993
 
994
+ class UploadRestartRequiredError extends Error {
995
+ constructor(
996
+ message: string,
997
+ readonly recoveryEnabled?: boolean,
998
+ ) {
999
+ super(message);
1000
+ this.name = "UploadRestartRequiredError";
1001
+ }
1002
+ }
1003
+
986
1004
  async function postBackupChunk(
987
1005
  url: string,
988
1006
  blob: Blob,
@@ -999,6 +1017,24 @@ async function postBackupChunk(
999
1017
  });
1000
1018
  const body = await res.text().catch(() => "");
1001
1019
  if (!res.ok) {
1020
+ const details = (() => {
1021
+ try {
1022
+ return JSON.parse(body) as {
1023
+ restartRequired?: unknown;
1024
+ recoveryEnabled?: unknown;
1025
+ };
1026
+ } catch {
1027
+ return null;
1028
+ }
1029
+ })();
1030
+ if (details?.restartRequired === true) {
1031
+ throw new UploadRestartRequiredError(
1032
+ `The prior upload session expired (${res.status})`,
1033
+ typeof details.recoveryEnabled === "boolean"
1034
+ ? details.recoveryEnabled
1035
+ : undefined,
1036
+ );
1037
+ }
1002
1038
  throw new Error(
1003
1039
  `Upload retry failed (${res.status}): ${body.slice(0, 200)}`,
1004
1040
  );
@@ -1009,7 +1045,9 @@ async function postBackupChunk(
1009
1045
  async function resetBrowserRecordingBackupUpload(
1010
1046
  meta: BrowserRecordingBackupMeta,
1011
1047
  authToken?: string,
1012
- ): Promise<UploadMode> {
1048
+ attemptId?: string,
1049
+ uploadGenerationId?: string,
1050
+ ): Promise<{ uploadMode: UploadMode; uploadGenerationId?: string }> {
1013
1051
  const res = await fetch(
1014
1052
  `${meta.serverUrl.replace(/\/+$/, "")}/api/uploads/${meta.recordingId}/reset-chunks`,
1015
1053
  {
@@ -1023,6 +1061,8 @@ async function resetBrowserRecordingBackupUpload(
1023
1061
  body: JSON.stringify({
1024
1062
  requestStreaming: true,
1025
1063
  mimeType: meta.mimeType,
1064
+ ...(attemptId ? { attemptId } : {}),
1065
+ ...(uploadGenerationId ? { uploadGenerationId } : {}),
1026
1066
  }),
1027
1067
  },
1028
1068
  );
@@ -1034,14 +1074,61 @@ async function resetBrowserRecordingBackupUpload(
1034
1074
  }
1035
1075
  const body = (await res.json().catch(() => null)) as {
1036
1076
  uploadMode?: unknown;
1077
+ uploadGenerationId?: unknown;
1037
1078
  } | null;
1038
- return body?.uploadMode === "streaming" ? "streaming" : "buffered";
1079
+ if (attemptId && typeof body?.uploadGenerationId !== "string") {
1080
+ throw new Error("Upload retry setup returned no upload generation");
1081
+ }
1082
+ return {
1083
+ uploadMode: body?.uploadMode === "streaming" ? "streaming" : "buffered",
1084
+ ...(typeof body?.uploadGenerationId === "string"
1085
+ ? { uploadGenerationId: body.uploadGenerationId }
1086
+ : {}),
1087
+ };
1088
+ }
1089
+
1090
+ async function getBrowserRecordingUploadResume(
1091
+ meta: BrowserRecordingBackupMeta,
1092
+ attemptId: string,
1093
+ authToken?: string,
1094
+ ): Promise<UploadResumeResponse> {
1095
+ const resumeUrl = new URL(
1096
+ `${meta.serverUrl.replace(/\/+$/, "")}/api/uploads/${meta.recordingId}/resume`,
1097
+ );
1098
+ resumeUrl.searchParams.set("attemptId", attemptId);
1099
+ const res = await fetch(resumeUrl, {
1100
+ method: "GET",
1101
+ headers: buildRetryHeaders("application/json", authToken),
1102
+ credentials: "include",
1103
+ });
1104
+ const body = await res.text().catch(() => "");
1105
+ if (!res.ok) {
1106
+ throw new Error(
1107
+ `Upload resume check failed (${res.status}): ${body.slice(0, 200)}`,
1108
+ );
1109
+ }
1110
+ let parsed: UploadResumeResponse;
1111
+ try {
1112
+ parsed = JSON.parse(body) as UploadResumeResponse;
1113
+ } catch {
1114
+ throw new Error("Upload resume check returned an unreadable response");
1115
+ }
1116
+ if (parsed.resumable && parsed.attemptId !== attemptId) {
1117
+ throw new Error("Upload resume check returned a mismatched retry token");
1118
+ }
1119
+ return parsed;
1039
1120
  }
1040
1121
 
1041
1122
  async function replayBrowserBackupToResumableSession(
1042
1123
  meta: BrowserRecordingBackupMeta,
1043
1124
  chunks: BrowserRecordingBackupChunk[],
1044
1125
  authToken?: string,
1126
+ attemptId?: string,
1127
+ uploadGenerationId?: string,
1128
+ resumeFrom: { bytesReceived: number; nextChunkIndex: number } = {
1129
+ bytesReceived: 0,
1130
+ nextChunkIndex: 0,
1131
+ },
1045
1132
  ): Promise<FinalizeReceipt | null> {
1046
1133
  // The backup is stored in raw MediaRecorder blobs, which have arbitrary
1047
1134
  // boundaries. A resumable provider needs every non-final request aligned,
@@ -1056,33 +1143,38 @@ async function replayBrowserBackupToResumableSession(
1056
1143
  throw new Error("Local recording backup is empty");
1057
1144
  }
1058
1145
 
1059
- const fullChunks = Math.floor(recording.size / STREAM_CHUNK_BYTES);
1060
- const totalPosts = fullChunks + 1;
1061
- let offset = 0;
1146
+ const replayPlan = buildStreamingReplayPlan({
1147
+ localBytes: recording.size,
1148
+ chunkBytes: STREAM_CHUNK_BYTES,
1149
+ ...resumeFrom,
1150
+ });
1151
+ const totalPosts = Math.floor(recording.size / STREAM_CHUNK_BYTES) + 1;
1062
1152
 
1063
- for (let index = 0; index < fullChunks; index += 1) {
1064
- const body = recording.slice(
1065
- offset,
1066
- offset + STREAM_CHUNK_BYTES,
1067
- meta.mimeType,
1068
- );
1153
+ for (const request of replayPlan.filter((item) => !item.final)) {
1154
+ const body = recording.slice(request.start, request.end, meta.mimeType);
1069
1155
  await postBackupChunk(
1070
- chunkUrl(meta.serverUrl, meta.recordingId, index, false, {
1156
+ chunkUrl(meta.serverUrl, meta.recordingId, request.index, false, {
1071
1157
  total: String(totalPosts),
1072
1158
  mimeType: meta.mimeType,
1159
+ ...(attemptId ? { attemptId } : {}),
1160
+ ...(uploadGenerationId ? { uploadGenerationId } : {}),
1073
1161
  }),
1074
1162
  body,
1075
1163
  authToken,
1076
1164
  );
1077
- offset += STREAM_CHUNK_BYTES;
1078
1165
  }
1079
1166
 
1080
1167
  // The final post always closes the session. When the file is exactly
1081
1168
  // aligned it is intentionally empty; the route sends the provider's close
1082
1169
  // request with the bytes committed by the previous chunks.
1083
- const finalBody = recording.slice(offset, recording.size, meta.mimeType);
1170
+ const finalRequest = replayPlan[replayPlan.length - 1]!;
1171
+ const finalBody = recording.slice(
1172
+ finalRequest.start,
1173
+ finalRequest.end,
1174
+ meta.mimeType,
1175
+ );
1084
1176
  return postBackupChunk(
1085
- chunkUrl(meta.serverUrl, meta.recordingId, fullChunks, true, {
1177
+ chunkUrl(meta.serverUrl, meta.recordingId, finalRequest.index, true, {
1086
1178
  total: String(totalPosts),
1087
1179
  mimeType: meta.mimeType,
1088
1180
  durationMs: String(Math.round(meta.durationMs || 0)),
@@ -1090,6 +1182,8 @@ async function replayBrowserBackupToResumableSession(
1090
1182
  ...(meta.height ? { height: String(meta.height) } : {}),
1091
1183
  hasAudio: meta.hasAudio ? "1" : "0",
1092
1184
  hasCamera: meta.hasCamera ? "1" : "0",
1185
+ ...(attemptId ? { attemptId } : {}),
1186
+ ...(uploadGenerationId ? { uploadGenerationId } : {}),
1093
1187
  }),
1094
1188
  finalBody,
1095
1189
  authToken,
@@ -1100,6 +1194,10 @@ export async function retryBrowserRecordingBackup(input: {
1100
1194
  recordingId: string;
1101
1195
  serverUrl?: string;
1102
1196
  authToken?: string;
1197
+ onRecoveryDecision?: (decision: {
1198
+ action: "resume" | "restart" | "reconcile";
1199
+ progress: number;
1200
+ }) => void;
1103
1201
  }): Promise<{ recordingId: string; viewUrl: string }> {
1104
1202
  let meta = await getBrowserRecordingBackupMeta(input.recordingId);
1105
1203
  if (!meta) {
@@ -1111,57 +1209,165 @@ export async function retryBrowserRecordingBackup(input: {
1111
1209
  }
1112
1210
  const chunks = await getBrowserRecordingBackupChunks(input.recordingId);
1113
1211
  const validatedChunks = validateBrowserRecordingBackupChunks(meta, chunks);
1212
+ let activeAttemptId: string | undefined =
1213
+ meta.uploadAttemptId || crypto.randomUUID();
1214
+ let activeUploadGenerationId: string | undefined;
1114
1215
 
1115
1216
  try {
1116
1217
  await putBrowserRecordingBackupMeta({
1117
1218
  ...meta,
1219
+ uploadAttemptId: activeAttemptId,
1118
1220
  lastAttemptAt: new Date().toISOString(),
1119
1221
  lastError: null,
1120
1222
  });
1121
- const uploadMode = await resetBrowserRecordingBackupUpload(
1223
+ const recordingBytes = validatedChunks.reduce(
1224
+ (total, chunk) => total + chunk.blob.size,
1225
+ 0,
1226
+ );
1227
+ const resumeResponse = await getBrowserRecordingUploadResume(
1122
1228
  meta,
1229
+ activeAttemptId,
1123
1230
  input.authToken,
1124
1231
  );
1232
+ const recoveryPlan = planStreamingRecovery({
1233
+ response: resumeResponse,
1234
+ localBytes: recordingBytes,
1235
+ chunkBytes: STREAM_CHUNK_BYTES,
1236
+ });
1237
+ activeAttemptId = retryAttemptIdAfterResumeResponse(
1238
+ activeAttemptId,
1239
+ resumeResponse,
1240
+ );
1241
+ activeUploadGenerationId = resumeResponse.resumable
1242
+ ? resumeResponse.uploadGenerationId
1243
+ : undefined;
1244
+ if (recoveryPlan.action === "reconcile") {
1245
+ input.onRecoveryDecision?.({ action: "reconcile", progress: 1 });
1246
+ if (
1247
+ await recoverAcceptedRecordingAfterFinalizeError({
1248
+ serverUrl: meta.serverUrl,
1249
+ recordingId: meta.recordingId,
1250
+ authToken: input.authToken,
1251
+ })
1252
+ ) {
1253
+ return {
1254
+ recordingId: meta.recordingId,
1255
+ viewUrl: `/r/${meta.recordingId}`,
1256
+ };
1257
+ }
1258
+ throw new Error(
1259
+ `Upload is ${recoveryPlan.status}, but its accepted media could not be verified`,
1260
+ );
1261
+ }
1262
+
1263
+ let uploadMode: UploadMode = "streaming";
1264
+ let resumeFrom = { bytesReceived: 0, nextChunkIndex: 0 };
1265
+ if (recoveryPlan.action === "resume") {
1266
+ resumeFrom = recoveryPlan;
1267
+ input.onRecoveryDecision?.({
1268
+ action: "resume",
1269
+ progress: recoveryPlan.progress,
1270
+ });
1271
+ console.info("[clips-recorder] resuming saved upload", {
1272
+ recordingId: meta.recordingId,
1273
+ localBytes: recordingBytes,
1274
+ serverAcknowledgedBytes: recoveryPlan.bytesReceived,
1275
+ nextChunkIndex: recoveryPlan.nextChunkIndex,
1276
+ progress: recoveryPlan.progress,
1277
+ });
1278
+ } else {
1279
+ input.onRecoveryDecision?.({ action: "restart", progress: 0 });
1280
+ console.info("[clips-recorder] restarting saved upload", {
1281
+ recordingId: meta.recordingId,
1282
+ localBytes: recordingBytes,
1283
+ reason: recoveryPlan.reason,
1284
+ });
1285
+ const reset = await resetBrowserRecordingBackupUpload(
1286
+ meta,
1287
+ input.authToken,
1288
+ activeAttemptId,
1289
+ activeUploadGenerationId,
1290
+ );
1291
+ uploadMode = reset.uploadMode;
1292
+ activeUploadGenerationId = reset.uploadGenerationId;
1293
+ }
1125
1294
 
1126
1295
  if (uploadMode === "streaming") {
1296
+ let receipt: FinalizeReceipt | null = null;
1127
1297
  try {
1128
- const receipt = await replayBrowserBackupToResumableSession(
1298
+ receipt = await replayBrowserBackupToResumableSession(
1129
1299
  meta,
1130
1300
  validatedChunks,
1131
1301
  input.authToken,
1302
+ activeAttemptId,
1303
+ activeUploadGenerationId,
1304
+ resumeFrom,
1132
1305
  );
1133
- const receiptStatus = verifyFinalizeReceipt(receipt, meta);
1134
- if (receiptStatus === "processing") {
1135
- scheduleBrowserBackupCleanupAfterProcessing({
1306
+ } catch (err) {
1307
+ if (err instanceof UploadRestartRequiredError) {
1308
+ activeAttemptId = retryAttemptIdAfterRestartSignal(
1309
+ activeAttemptId,
1310
+ err.recoveryEnabled,
1311
+ );
1312
+ if (err.recoveryEnabled === false) {
1313
+ activeUploadGenerationId = undefined;
1314
+ }
1315
+ input.onRecoveryDecision?.({ action: "restart", progress: 0 });
1316
+ console.info("[clips-recorder] restarting expired upload session", {
1317
+ recordingId: meta.recordingId,
1318
+ localBytes: recordingBytes,
1319
+ });
1320
+ const reset = await resetBrowserRecordingBackupUpload(
1321
+ meta,
1322
+ input.authToken,
1323
+ activeAttemptId,
1324
+ activeUploadGenerationId,
1325
+ );
1326
+ uploadMode = reset.uploadMode;
1327
+ activeUploadGenerationId = reset.uploadGenerationId;
1328
+ if (uploadMode === "streaming") {
1329
+ receipt = await replayBrowserBackupToResumableSession(
1330
+ meta,
1331
+ validatedChunks,
1332
+ input.authToken,
1333
+ activeAttemptId,
1334
+ activeUploadGenerationId,
1335
+ );
1336
+ }
1337
+ } else if (
1338
+ await recoverAcceptedRecordingAfterFinalizeError({
1136
1339
  serverUrl: meta.serverUrl,
1137
1340
  recordingId: meta.recordingId,
1138
1341
  authToken: input.authToken,
1139
- });
1342
+ })
1343
+ ) {
1140
1344
  return {
1141
1345
  recordingId: meta.recordingId,
1142
1346
  viewUrl: `/r/${meta.recordingId}`,
1143
1347
  };
1348
+ } else {
1349
+ throw err;
1144
1350
  }
1145
- } catch (err) {
1146
- if (
1147
- await recoverAcceptedRecordingAfterFinalizeError({
1351
+ }
1352
+ if (uploadMode === "streaming") {
1353
+ const receiptStatus = verifyFinalizeReceipt(receipt, meta);
1354
+ if (receiptStatus === "processing") {
1355
+ scheduleBrowserBackupCleanupAfterProcessing({
1148
1356
  serverUrl: meta.serverUrl,
1149
1357
  recordingId: meta.recordingId,
1150
1358
  authToken: input.authToken,
1151
- })
1152
- ) {
1359
+ });
1153
1360
  return {
1154
1361
  recordingId: meta.recordingId,
1155
1362
  viewUrl: `/r/${meta.recordingId}`,
1156
1363
  };
1157
1364
  }
1158
- throw err;
1365
+ await deleteBrowserRecordingBackup(meta.recordingId);
1366
+ return {
1367
+ recordingId: meta.recordingId,
1368
+ viewUrl: `/r/${meta.recordingId}`,
1369
+ };
1159
1370
  }
1160
- await deleteBrowserRecordingBackup(meta.recordingId);
1161
- return {
1162
- recordingId: meta.recordingId,
1163
- viewUrl: `/r/${meta.recordingId}`,
1164
- };
1165
1371
  }
1166
1372
 
1167
1373
  const totalPosts = validatedChunks.length + 1;
@@ -1170,6 +1376,10 @@ export async function retryBrowserRecordingBackup(input: {
1170
1376
  chunkUrl(meta.serverUrl, meta.recordingId, chunk.index, false, {
1171
1377
  total: String(totalPosts),
1172
1378
  mimeType: meta.mimeType,
1379
+ ...(activeAttemptId ? { attemptId: activeAttemptId } : {}),
1380
+ ...(activeUploadGenerationId
1381
+ ? { uploadGenerationId: activeUploadGenerationId }
1382
+ : {}),
1173
1383
  }),
1174
1384
  chunk.blob,
1175
1385
  input.authToken,
@@ -1189,6 +1399,10 @@ export async function retryBrowserRecordingBackup(input: {
1189
1399
  ...(meta.height ? { height: String(meta.height) } : {}),
1190
1400
  hasAudio: meta.hasAudio ? "1" : "0",
1191
1401
  hasCamera: meta.hasCamera ? "1" : "0",
1402
+ ...(activeAttemptId ? { attemptId: activeAttemptId } : {}),
1403
+ ...(activeUploadGenerationId
1404
+ ? { uploadGenerationId: activeUploadGenerationId }
1405
+ : {}),
1192
1406
  },
1193
1407
  );
1194
1408
  try {
@@ -1229,9 +1443,29 @@ export async function retryBrowserRecordingBackup(input: {
1229
1443
  return { recordingId: meta.recordingId, viewUrl: `/r/${meta.recordingId}` };
1230
1444
  } catch (err) {
1231
1445
  const message = err instanceof Error ? err.message : String(err);
1446
+ if (
1447
+ await recoverAcceptedRecordingAfterFinalizeError({
1448
+ serverUrl: meta.serverUrl,
1449
+ recordingId: meta.recordingId,
1450
+ authToken: input.authToken,
1451
+ })
1452
+ ) {
1453
+ return {
1454
+ recordingId: meta.recordingId,
1455
+ viewUrl: `/r/${meta.recordingId}`,
1456
+ };
1457
+ }
1232
1458
  await markBrowserRecordingBackupError(meta.recordingId, message).catch(
1233
1459
  () => {},
1234
1460
  );
1461
+ await interruptRecordingUpload(
1462
+ meta.serverUrl,
1463
+ meta.recordingId,
1464
+ message,
1465
+ input.authToken,
1466
+ activeAttemptId,
1467
+ activeUploadGenerationId,
1468
+ );
1235
1469
  throw err;
1236
1470
  }
1237
1471
  }
@@ -1610,6 +1844,55 @@ async function abortRecordingUpload(
1610
1844
  }
1611
1845
  }
1612
1846
 
1847
+ async function interruptRecordingUpload(
1848
+ serverUrl: string,
1849
+ recordingId: string,
1850
+ detail: string,
1851
+ authToken?: string,
1852
+ attemptId?: string,
1853
+ uploadGenerationId?: string,
1854
+ ): Promise<void> {
1855
+ let lastError: unknown = null;
1856
+ for (let attempt = 1; attempt <= CHUNK_UPLOAD_MAX_ATTEMPTS; attempt += 1) {
1857
+ try {
1858
+ const res = await fetch(
1859
+ `${serverUrl.replace(/\/+$/, "")}/api/uploads/${recordingId}/interrupt`,
1860
+ {
1861
+ method: "POST",
1862
+ headers: buildRetryHeaders("application/json", authToken),
1863
+ credentials: "include",
1864
+ body: JSON.stringify({
1865
+ detail,
1866
+ ...(attemptId ? { attemptId } : {}),
1867
+ ...(uploadGenerationId ? { uploadGenerationId } : {}),
1868
+ }),
1869
+ },
1870
+ );
1871
+ if (res.ok) {
1872
+ console.info("[clips-recorder] upload interruption recorded", {
1873
+ recordingId,
1874
+ attempt,
1875
+ });
1876
+ return;
1877
+ }
1878
+ const body = await res.text().catch(() => "");
1879
+ lastError = new Error(
1880
+ `Upload interruption failed (${res.status}): ${body.slice(0, 200)}`,
1881
+ );
1882
+ if (!isRetriableChunkStatus(res.status)) break;
1883
+ } catch (err) {
1884
+ lastError = err;
1885
+ }
1886
+ if (attempt < CHUNK_UPLOAD_MAX_ATTEMPTS) {
1887
+ await wait(CHUNK_UPLOAD_RETRY_BASE_MS * 2 ** (attempt - 1));
1888
+ }
1889
+ }
1890
+ console.warn(
1891
+ "[clips-recorder] upload interruption could not be recorded:",
1892
+ lastError,
1893
+ );
1894
+ }
1895
+
1613
1896
  async function trashRecording(
1614
1897
  serverUrl: string,
1615
1898
  recordingId: string,
@@ -2427,10 +2710,11 @@ async function tryStartRewindFullscreenRecording(
2427
2710
  ) {
2428
2711
  return { recordingId: id, viewUrl };
2429
2712
  }
2430
- await abortRecordingUpload(
2713
+ await interruptRecordingUpload(
2431
2714
  params.serverUrl,
2432
2715
  id,
2433
2716
  err instanceof Error ? err.message : String(err),
2717
+ params.authToken,
2434
2718
  );
2435
2719
  throw err;
2436
2720
  }
@@ -3101,10 +3385,11 @@ async function startNativeFullscreenRecording(
3101
3385
  ) {
3102
3386
  return { recordingId: id, viewUrl };
3103
3387
  }
3104
- await abortRecordingUpload(
3388
+ await interruptRecordingUpload(
3105
3389
  params.serverUrl,
3106
3390
  id,
3107
3391
  err instanceof Error ? err.message : String(err),
3392
+ params.authToken,
3108
3393
  );
3109
3394
  throw err;
3110
3395
  }
@@ -4453,7 +4738,12 @@ async function startRecordingInner(
4453
4738
  await markBrowserRecordingBackupError(id, failed.message).catch(
4454
4739
  () => {},
4455
4740
  );
4456
- await abortRecordingUpload(params.serverUrl, id, failed.message);
4741
+ await interruptRecordingUpload(
4742
+ params.serverUrl,
4743
+ id,
4744
+ failed.message,
4745
+ params.authToken,
4746
+ );
4457
4747
  } finally {
4458
4748
  await clearRecordingState();
4459
4749
  await publishFinalizingResult({
@@ -4559,7 +4849,12 @@ async function startRecordingInner(
4559
4849
  await markBrowserRecordingBackupError(id, error.message).catch(
4560
4850
  () => {},
4561
4851
  );
4562
- await abortRecordingUpload(params.serverUrl, id, error.message);
4852
+ await interruptRecordingUpload(
4853
+ params.serverUrl,
4854
+ id,
4855
+ error.message,
4856
+ params.authToken,
4857
+ );
4563
4858
  throw error;
4564
4859
  }
4565
4860
  await deleteBrowserRecordingBackup(id).catch((err) => {
@@ -0,0 +1,129 @@
1
+ export type UploadResumeResponse =
2
+ | {
3
+ resumable: true;
4
+ recoveryEnabled: boolean;
5
+ status: string;
6
+ uploadMode: "streaming" | "buffered";
7
+ attemptId: string;
8
+ uploadGenerationId?: string;
9
+ bytesReceived: number;
10
+ nextChunkIndex: number;
11
+ }
12
+ | {
13
+ resumable: false;
14
+ recoveryEnabled: boolean;
15
+ status: string | null;
16
+ failureReason?: string | null;
17
+ videoUrl?: string | null;
18
+ reason?: string;
19
+ attemptId?: string;
20
+ };
21
+
22
+ export type StreamingRecoveryPlan =
23
+ | {
24
+ action: "resume";
25
+ bytesReceived: number;
26
+ nextChunkIndex: number;
27
+ progress: number;
28
+ }
29
+ | { action: "reconcile"; status: "ready" | "processing" }
30
+ | { action: "restart"; reason: string };
31
+
32
+ export interface StreamingReplayRequest {
33
+ index: number;
34
+ start: number;
35
+ end: number;
36
+ final: boolean;
37
+ }
38
+
39
+ export function retryAttemptIdAfterRestartSignal(
40
+ attemptId: string | undefined,
41
+ recoveryEnabled: unknown,
42
+ ): string | undefined {
43
+ return recoveryEnabled === false ? undefined : attemptId;
44
+ }
45
+
46
+ export function retryAttemptIdAfterResumeResponse(
47
+ attemptId: string | undefined,
48
+ response: UploadResumeResponse,
49
+ ): string | undefined {
50
+ return response.resumable && response.attemptId === attemptId
51
+ ? attemptId
52
+ : undefined;
53
+ }
54
+
55
+ export function buildStreamingReplayPlan(input: {
56
+ localBytes: number;
57
+ chunkBytes: number;
58
+ bytesReceived: number;
59
+ nextChunkIndex: number;
60
+ }): StreamingReplayRequest[] {
61
+ const { localBytes, chunkBytes, bytesReceived, nextChunkIndex } = input;
62
+ const fullChunks = Math.floor(localBytes / chunkBytes);
63
+ const requests: StreamingReplayRequest[] = [];
64
+ let offset = bytesReceived;
65
+ for (let index = nextChunkIndex; index < fullChunks; index += 1) {
66
+ requests.push({
67
+ index,
68
+ start: offset,
69
+ end: offset + chunkBytes,
70
+ final: false,
71
+ });
72
+ offset += chunkBytes;
73
+ }
74
+ requests.push({
75
+ index: fullChunks,
76
+ start: offset,
77
+ end: localBytes,
78
+ final: true,
79
+ });
80
+ return requests;
81
+ }
82
+
83
+ export function planStreamingRecovery(input: {
84
+ response: UploadResumeResponse;
85
+ localBytes: number;
86
+ chunkBytes: number;
87
+ }): StreamingRecoveryPlan {
88
+ const { response, localBytes, chunkBytes } = input;
89
+ if (!response.recoveryEnabled) {
90
+ return { action: "restart", reason: "resumable retry is disabled" };
91
+ }
92
+ if (response.status === "ready" || response.status === "processing") {
93
+ return { action: "reconcile", status: response.status };
94
+ }
95
+ if (!response.resumable) {
96
+ return {
97
+ action: "restart",
98
+ reason: response.reason ?? "server session is unavailable",
99
+ };
100
+ }
101
+ if (response.uploadMode !== "streaming") {
102
+ return { action: "restart", reason: "server session is not streaming" };
103
+ }
104
+ if (!response.attemptId) {
105
+ return { action: "restart", reason: "server retry token is missing" };
106
+ }
107
+ const { bytesReceived, nextChunkIndex } = response;
108
+ if (
109
+ !Number.isSafeInteger(bytesReceived) ||
110
+ bytesReceived < 0 ||
111
+ bytesReceived > localBytes
112
+ ) {
113
+ return { action: "restart", reason: "server byte offset is invalid" };
114
+ }
115
+ if (
116
+ !Number.isSafeInteger(nextChunkIndex) ||
117
+ nextChunkIndex < 0 ||
118
+ bytesReceived % chunkBytes !== 0 ||
119
+ nextChunkIndex !== bytesReceived / chunkBytes
120
+ ) {
121
+ return { action: "restart", reason: "server chunk offset is inconsistent" };
122
+ }
123
+ return {
124
+ action: "resume",
125
+ bytesReceived,
126
+ nextChunkIndex,
127
+ progress: localBytes > 0 ? bytesReceived / localBytes : 0,
128
+ };
129
+ }