@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
@@ -193,6 +193,12 @@ const migrations = runMigrations(
193
193
  description TEXT NOT NULL DEFAULT '',
194
194
  thumbnail_url TEXT,
195
195
  animated_thumbnail_url TEXT,
196
+ filmstrip_url TEXT,
197
+ filmstrip_frame_count INTEGER NOT NULL DEFAULT 0,
198
+ filmstrip_columns INTEGER NOT NULL DEFAULT 0,
199
+ filmstrip_rows INTEGER NOT NULL DEFAULT 0,
200
+ filmstrip_frame_width INTEGER NOT NULL DEFAULT 0,
201
+ filmstrip_frame_height INTEGER NOT NULL DEFAULT 0,
196
202
  duration_ms INTEGER NOT NULL DEFAULT 0,
197
203
  video_url TEXT,
198
204
  video_format TEXT NOT NULL DEFAULT 'webm',
@@ -899,6 +905,16 @@ const migrations = runMigrations(
899
905
  `UPDATE recordings SET upload_lease_expires_at = '${uploadLeaseExpiry()}' WHERE upload_lease_expires_at IS NULL AND status IN ('uploading', 'processing')`,
900
906
  ].join("; "),
901
907
  },
908
+ {
909
+ version: 54,
910
+ name: "recording-upload-attempt-fence",
911
+ sql: `ALTER TABLE recordings ADD COLUMN IF NOT EXISTS upload_attempt_id TEXT`,
912
+ },
913
+ {
914
+ version: 55,
915
+ name: "recording-upload-generation-fence",
916
+ sql: `ALTER TABLE recordings ADD COLUMN IF NOT EXISTS upload_generation_id TEXT`,
917
+ },
902
918
  ],
903
919
  { table: "clips_migrations" },
904
920
  );
@@ -66,6 +66,7 @@ export default defineEventHandler(async (event: H3Event) => {
66
66
  ownerEmail: schema.recordings.ownerEmail,
67
67
  orgId: schema.recordings.orgId,
68
68
  status: schema.recordings.status,
69
+ uploadGenerationId: schema.recordings.uploadGenerationId,
69
70
  })
70
71
  .from(schema.recordings)
71
72
  .where(eq(schema.recordings.id, recordingId))
@@ -120,6 +121,9 @@ export default defineEventHandler(async (event: H3Event) => {
120
121
  const result = await finalizeRecording.run({
121
122
  id: recordingId,
122
123
  mediaVerificationRetryAttempt: retryAttempt ?? 1,
124
+ ...(recording.uploadGenerationId
125
+ ? { uploadGenerationId: recording.uploadGenerationId }
126
+ : {}),
123
127
  });
124
128
  return { ok: true, kind, result };
125
129
  }
@@ -6,14 +6,13 @@
6
6
  */
7
7
 
8
8
  import {
9
- deleteAppState,
9
+ compareAndSetManyAppState,
10
10
  readAppState,
11
11
  writeAppState,
12
- deleteAppStateByPrefix,
13
12
  } from "@agent-native/core/application-state";
14
13
  import { runWithRequestContext } from "@agent-native/core/server";
15
14
  import { isStoredButUnservableFinalizeError } from "@shared/finalize-recovery.js";
16
- import { and, eq } from "drizzle-orm";
15
+ import { and, eq, isNull } from "drizzle-orm";
17
16
  import {
18
17
  defineEventHandler,
19
18
  getRouterParam,
@@ -24,6 +23,7 @@ import {
24
23
 
25
24
  import { getDb, schema } from "../../../../db/index.js";
26
25
  import { mediaVerificationStateKey } from "../../../../lib/media-verification-state.js";
26
+ import { deleteRecordingChunks } from "../../../../lib/recording-upload-state.js";
27
27
  import {
28
28
  getEventOwnerContext,
29
29
  ownerEmailMatches,
@@ -44,11 +44,25 @@ export default defineEventHandler(async (event: H3Event) => {
44
44
  const { userEmail: ownerEmail, orgId } = await getEventOwnerContext(event);
45
45
  const body = (await readBody(event).catch(() => null)) as {
46
46
  reason?: unknown;
47
+ attemptId?: unknown;
48
+ uploadGenerationId?: unknown;
47
49
  } | null;
48
50
  const failureReason =
49
51
  typeof body?.reason === "string" && body.reason.trim()
50
52
  ? body.reason.trim().slice(0, 1000)
51
53
  : "Upload aborted by user";
54
+ const requestedAttemptId =
55
+ typeof body?.attemptId === "string" &&
56
+ body.attemptId.length > 0 &&
57
+ body.attemptId.length <= 128
58
+ ? body.attemptId
59
+ : null;
60
+ const requestedGenerationId =
61
+ typeof body?.uploadGenerationId === "string" &&
62
+ body.uploadGenerationId.length > 0 &&
63
+ body.uploadGenerationId.length <= 128
64
+ ? body.uploadGenerationId
65
+ : null;
52
66
 
53
67
  return runWithRequestContext({ userEmail: ownerEmail, orgId }, async () => {
54
68
  const db = getDb();
@@ -59,6 +73,8 @@ export default defineEventHandler(async (event: H3Event) => {
59
73
  status: schema.recordings.status,
60
74
  videoUrl: schema.recordings.videoUrl,
61
75
  failureReason: schema.recordings.failureReason,
76
+ uploadAttemptId: schema.recordings.uploadAttemptId,
77
+ uploadGenerationId: schema.recordings.uploadGenerationId,
62
78
  })
63
79
  .from(schema.recordings)
64
80
  .where(
@@ -73,17 +89,43 @@ export default defineEventHandler(async (event: H3Event) => {
73
89
  return { error: "Recording not found" };
74
90
  }
75
91
 
92
+ const existingAttemptId = existing.uploadAttemptId ?? null;
93
+ const existingGenerationId = existing.uploadGenerationId ?? null;
94
+ if (
95
+ requestedAttemptId !== existingAttemptId ||
96
+ requestedGenerationId !== existingGenerationId
97
+ ) {
98
+ setResponseStatus(event, 409);
99
+ return {
100
+ error: "A newer upload retry is already active.",
101
+ staleAttempt: true,
102
+ };
103
+ }
104
+
76
105
  if (existing.status === "ready" && existing.videoUrl) {
77
106
  return { ok: true, recordingId, alreadyReady: true, chunksCleared: 0 };
78
107
  }
79
108
 
80
- const existingUploadStateRaw = await readAppState(
81
- `recording-upload-${recordingId}`,
82
- ).catch(() => null);
109
+ const uploadStateKey = `recording-upload-${recordingId}`;
110
+ const verificationStateKey = mediaVerificationStateKey(recordingId);
111
+ const [existingUploadStateRaw, existingVerificationStateRaw] =
112
+ await Promise.all([
113
+ readAppState(uploadStateKey),
114
+ readAppState(verificationStateKey),
115
+ ]);
83
116
  const existingUploadState =
84
117
  existingUploadStateRaw && typeof existingUploadStateRaw === "object"
85
118
  ? (existingUploadStateRaw as Record<string, unknown>)
86
119
  : {};
120
+ const existingUploadStateSnapshot =
121
+ existingUploadStateRaw && typeof existingUploadStateRaw === "object"
122
+ ? (existingUploadStateRaw as Record<string, unknown>)
123
+ : null;
124
+ const existingVerificationStateSnapshot =
125
+ existingVerificationStateRaw &&
126
+ typeof existingVerificationStateRaw === "object"
127
+ ? (existingVerificationStateRaw as Record<string, unknown>)
128
+ : null;
87
129
  if (
88
130
  existing.status === "processing" &&
89
131
  existingUploadState.pendingMediaVerification === true
@@ -99,46 +141,87 @@ export default defineEventHandler(async (event: H3Event) => {
99
141
  const preserveRecoveryState =
100
142
  isStoredButUnservableFinalizeError(failureReason) ||
101
143
  isStoredButUnservableFinalizeError(existing.failureReason);
102
- const resumableSession = preserveRecoveryState
144
+ let resumableSession = preserveRecoveryState
103
145
  ? null
104
- : await getResumableSession(recordingId).catch(() => null);
105
-
106
- // Already a terminal failure (e.g. a duplicate/retried abort call, or
107
- // finalize's own failChunkAssembly already flipped it) — no-op unless a
108
- // prior provider cleanup failure intentionally retained a resumable
109
- // session for this retry.
110
- if (
111
- existing.status === "failed" &&
112
- !preserveRecoveryState &&
113
- !resumableSession
114
- ) {
115
- return { ok: true, recordingId, alreadyFailed: true, chunksCleared: 0 };
116
- }
146
+ : await getResumableSession(recordingId, existingGenerationId);
117
147
 
118
148
  const now = new Date().toISOString();
119
- await db
149
+ const aborted = await db
120
150
  .update(schema.recordings)
121
151
  .set({
122
152
  status: "failed",
123
153
  failureReason,
124
154
  updatedAt: now,
125
155
  })
126
- .where(eq(schema.recordings.id, recordingId));
127
-
128
- await writeAppState(`recording-upload-${recordingId}`, {
129
- ...existingUploadState,
130
- recordingId,
131
- status: "failed",
132
- failureReason,
133
- updatedAt: now,
134
- });
135
- await deleteAppState(mediaVerificationStateKey(recordingId)).catch(
136
- () => {},
137
- );
156
+ .where(
157
+ and(
158
+ eq(schema.recordings.id, recordingId),
159
+ ownerEmailMatches(schema.recordings.ownerEmail, ownerEmail),
160
+ eq(schema.recordings.status, existing.status),
161
+ existingAttemptId === null
162
+ ? isNull(schema.recordings.uploadAttemptId)
163
+ : eq(schema.recordings.uploadAttemptId, existingAttemptId),
164
+ existingGenerationId === null
165
+ ? isNull(schema.recordings.uploadGenerationId)
166
+ : eq(schema.recordings.uploadGenerationId, existingGenerationId),
167
+ ),
168
+ )
169
+ .returning({ id: schema.recordings.id });
170
+
171
+ if (aborted.length !== 1) {
172
+ setResponseStatus(event, 409);
173
+ return {
174
+ error: "A newer upload retry is already active.",
175
+ staleAttempt: true,
176
+ };
177
+ }
178
+
179
+ const auxiliaryStateUpdated = await compareAndSetManyAppState([
180
+ {
181
+ key: uploadStateKey,
182
+ expectedValue: existingUploadStateSnapshot,
183
+ nextValue: {
184
+ ...existingUploadState,
185
+ recordingId,
186
+ status: "failed",
187
+ failureReason,
188
+ updatedAt: now,
189
+ },
190
+ },
191
+ ...(existingVerificationStateSnapshot
192
+ ? [
193
+ {
194
+ key: verificationStateKey,
195
+ expectedValue: existingVerificationStateSnapshot,
196
+ nextValue: null,
197
+ },
198
+ ]
199
+ : []),
200
+ ]);
201
+ if (!auxiliaryStateUpdated) {
202
+ console.info(
203
+ `[abort] upload state changed after abort claim; preserving replacement state for ${recordingId}`,
204
+ );
205
+ }
206
+
207
+ // Reset may have started this exact generation's provider session after
208
+ // our preflight read but before the abort claim. Re-read after the row CAS
209
+ // so either abort observes the late handle or reset observes the lost row
210
+ // claim and compensates it.
211
+ if (!preserveRecoveryState) {
212
+ resumableSession = await getResumableSession(
213
+ recordingId,
214
+ existingGenerationId,
215
+ );
216
+ }
138
217
 
139
218
  const cleared = preserveRecoveryState
140
219
  ? 0
141
- : await deleteAppStateByPrefix(`recording-chunks-${recordingId}-`);
220
+ : await deleteRecordingChunks(
221
+ ownerEmail,
222
+ recordingId,
223
+ existingGenerationId,
224
+ );
142
225
  if (!preserveRecoveryState) {
143
226
  if (resumableSession) {
144
227
  const provider = await resolveResumableUploadProvider(
@@ -166,10 +249,14 @@ export default defineEventHandler(async (event: H3Event) => {
166
249
  // retry can still address the multipart upload. Deleting it here would
167
250
  // permanently orphan the provider-side session.
168
251
  if (providerCleanupSucceeded) {
169
- await deleteResumableSession(recordingId).catch(() => {});
252
+ await deleteResumableSession(recordingId, existingGenerationId).catch(
253
+ () => {},
254
+ );
170
255
  }
171
256
  } else {
172
- await deleteResumableSession(recordingId).catch(() => {});
257
+ await deleteResumableSession(recordingId, existingGenerationId).catch(
258
+ () => {},
259
+ );
173
260
  }
174
261
  }
175
262
  await writeAppState("refresh-signal", { ts: Date.now() });
@@ -17,6 +17,7 @@ import {
17
17
  readAppState,
18
18
  writeAppState,
19
19
  } from "@agent-native/core/application-state";
20
+ import { isFeatureFlagEnabled } from "@agent-native/core/feature-flags";
20
21
  import { runWithRequestContext } from "@agent-native/core/server";
21
22
  import { track } from "@agent-native/core/tracking";
22
23
  import { normalizeChunkUploadNumber } from "@shared/recording-core.js";
@@ -34,6 +35,7 @@ import {
34
35
  } from "h3";
35
36
 
36
37
  import finalizeRecording from "../../../../../actions/finalize-recording.js";
38
+ import { UPLOAD_RETRY_RESUME_FLAG } from "../../../../../shared/feature-flags.js";
37
39
  import { getDb, schema } from "../../../../db/index.js";
38
40
  import { debugLog } from "../../../../lib/debug.js";
39
41
  import {
@@ -45,6 +47,7 @@ import {
45
47
  ownerEmailMatches,
46
48
  } from "../../../../lib/recordings.js";
47
49
  import {
50
+ deleteResumableSession,
48
51
  getResumableSession,
49
52
  setResumableSession,
50
53
  type StoredResumableSession,
@@ -166,6 +169,26 @@ export default defineEventHandler(async (event: H3Event) => {
166
169
  }
167
170
 
168
171
  const query = getQuery(event);
172
+ const attemptIdValue = Array.isArray(query.attemptId)
173
+ ? query.attemptId[0]
174
+ : query.attemptId;
175
+ const attemptId =
176
+ typeof attemptIdValue === "string" &&
177
+ attemptIdValue.length > 0 &&
178
+ attemptIdValue.length <= 128 &&
179
+ !/[\r\n]/.test(attemptIdValue)
180
+ ? attemptIdValue
181
+ : null;
182
+ const uploadGenerationIdValue = Array.isArray(query.uploadGenerationId)
183
+ ? query.uploadGenerationId[0]
184
+ : query.uploadGenerationId;
185
+ const uploadGenerationId =
186
+ typeof uploadGenerationIdValue === "string" &&
187
+ uploadGenerationIdValue.length > 0 &&
188
+ uploadGenerationIdValue.length <= 128 &&
189
+ !/[\r\n]/.test(uploadGenerationIdValue)
190
+ ? uploadGenerationIdValue
191
+ : null;
169
192
  const index = Number(query.index ?? 0);
170
193
  const total = Number(query.total ?? 0);
171
194
  const isFinal = query.isFinal === "1" || query.isFinal === "true";
@@ -188,7 +211,12 @@ export default defineEventHandler(async (event: H3Event) => {
188
211
  mimeType,
189
212
  });
190
213
 
191
- if (!Number.isFinite(index) || !Number.isInteger(index) || index < 0) {
214
+ if (
215
+ !Number.isFinite(index) ||
216
+ !Number.isInteger(index) ||
217
+ index < 0 ||
218
+ index > 999_999
219
+ ) {
192
220
  throw createError({ statusCode: 400, message: "Invalid chunk index" });
193
221
  }
194
222
 
@@ -210,6 +238,23 @@ export default defineEventHandler(async (event: H3Event) => {
210
238
  }
211
239
  debugLog("[chunk] resolved owner:", ownerEmail);
212
240
 
241
+ if (
242
+ attemptId !== null &&
243
+ !(await isFeatureFlagEnabled(UPLOAD_RETRY_RESUME_FLAG, {
244
+ userEmail: ownerEmail,
245
+ userKey: ownerEmail,
246
+ orgId,
247
+ }))
248
+ ) {
249
+ setResponseStatus(event, 409);
250
+ return {
251
+ ok: false,
252
+ error: "Resumable upload retry is disabled.",
253
+ restartRequired: true,
254
+ recoveryEnabled: false,
255
+ };
256
+ }
257
+
213
258
  return runWithRequestContext({ userEmail: ownerEmail, orgId }, async () => {
214
259
  const db = getDb();
215
260
 
@@ -219,6 +264,7 @@ export default defineEventHandler(async (event: H3Event) => {
219
264
  .select({
220
265
  id: schema.recordings.id,
221
266
  status: schema.recordings.status,
267
+ uploadGenerationId: schema.recordings.uploadGenerationId,
222
268
  })
223
269
  .from(schema.recordings)
224
270
  .where(
@@ -252,13 +298,31 @@ export default defineEventHandler(async (event: H3Event) => {
252
298
  // The first renewal admits the request. Later renewals immediately before
253
299
  // every durable write/finalization boundary close the body-read/provider
254
300
  // gap where /abort can otherwise commit while this request is in flight.
301
+ if ((existing.uploadGenerationId ?? null) !== uploadGenerationId) {
302
+ setResponseStatus(event, 409);
303
+ return {
304
+ ok: false,
305
+ error: "A newer upload generation is already active.",
306
+ staleAttempt: true,
307
+ };
308
+ }
255
309
  const lease = await renewUploadLease(recordingId, {
310
+ attemptId,
311
+ generationId: uploadGenerationId,
256
312
  uploadProgress:
257
313
  total > 0
258
314
  ? Math.min(100, Math.round(((index + 1) / total) * 100))
259
315
  : undefined,
260
316
  });
261
317
  if (!lease.held) {
318
+ if (lease.staleAttempt) {
319
+ setResponseStatus(event, 409);
320
+ return {
321
+ ok: false,
322
+ error: "A newer upload retry is already active.",
323
+ staleAttempt: true,
324
+ };
325
+ }
262
326
  if (lease.status === "ready") {
263
327
  const readyState = await readAppState(
264
328
  `recording-upload-${recordingId}`,
@@ -273,7 +337,11 @@ export default defineEventHandler(async (event: H3Event) => {
273
337
  sourceSizeBytes: stateNumber(readyState, "sourceSizeBytes"),
274
338
  };
275
339
  }
276
- await deleteRecordingChunks(ownerEmail, recordingId).catch((err) => {
340
+ await deleteRecordingChunks(
341
+ ownerEmail,
342
+ recordingId,
343
+ uploadGenerationId,
344
+ ).catch((err) => {
277
345
  console.warn("[chunk] failed upload chunk cleanup failed:", {
278
346
  recordingId,
279
347
  err: err instanceof Error ? err.message : String(err),
@@ -285,9 +353,16 @@ export default defineEventHandler(async (event: H3Event) => {
285
353
  }
286
354
 
287
355
  const rejectIfLeaseLost = async () => {
288
- const current = await renewUploadLease(recordingId);
356
+ const current = await renewUploadLease(recordingId, {
357
+ attemptId,
358
+ generationId: uploadGenerationId,
359
+ });
289
360
  if (current.held) return null;
290
- await deleteRecordingChunks(ownerEmail, recordingId).catch((err) => {
361
+ await deleteRecordingChunks(
362
+ ownerEmail,
363
+ recordingId,
364
+ uploadGenerationId,
365
+ ).catch((err) => {
291
366
  console.warn("[chunk] failed upload chunk cleanup failed:", {
292
367
  recordingId,
293
368
  err: err instanceof Error ? err.message : String(err),
@@ -308,7 +383,10 @@ export default defineEventHandler(async (event: H3Event) => {
308
383
  }
309
384
 
310
385
  // Resumable streaming path — forward chunks directly to the provider.
311
- const resumableSession = await getResumableSession(recordingId);
386
+ const resumableSession = await getResumableSession(
387
+ recordingId,
388
+ uploadGenerationId,
389
+ );
312
390
  if (resumableSession && isStreamingUploadDisabled()) {
313
391
  console.warn(
314
392
  `[chunk] streaming uploads are disabled, but preserving existing resumable session for in-flight recording: ${recordingId}`,
@@ -324,6 +402,8 @@ export default defineEventHandler(async (event: H3Event) => {
324
402
  mimeType,
325
403
  query,
326
404
  ownerEmail,
405
+ attemptId,
406
+ uploadGenerationId,
327
407
  );
328
408
  }
329
409
 
@@ -418,7 +498,11 @@ export default defineEventHandler(async (event: H3Event) => {
418
498
  maxBytes: MAX_RECORDING_UPLOAD_BYTES,
419
499
  updatedAt: now,
420
500
  });
421
- await deleteRecordingChunks(ownerEmail, recordingId).catch((err) => {
501
+ await deleteRecordingChunks(
502
+ ownerEmail,
503
+ recordingId,
504
+ uploadGenerationId,
505
+ ).catch((err) => {
422
506
  console.warn("[chunk] oversized upload chunk cleanup failed:", {
423
507
  recordingId,
424
508
  err: err instanceof Error ? err.message : String(err),
@@ -439,12 +523,13 @@ export default defineEventHandler(async (event: H3Event) => {
439
523
  // Pad index to 6 digits so string-sort order matches numeric order if the
440
524
  // finalize path ever sorts lexically. (finalize also parses back to a number.)
441
525
  const paddedIndex = String(index).padStart(6, "0");
442
- const chunkKey = `recording-chunks-${recordingId}-${paddedIndex}`;
526
+ const chunkKey = `recording-chunks-${recordingId}${uploadGenerationId ? `-${uploadGenerationId}` : ""}-${paddedIndex}`;
443
527
  const previousChunk = await readAppState(chunkKey);
444
528
  const previousBytes = stateNumber(previousChunk, "bytes") ?? 0;
445
529
  const persistedBytesBefore = await sumRecordingChunkBytes(
446
530
  ownerEmail,
447
531
  recordingId,
532
+ uploadGenerationId,
448
533
  );
449
534
  const nextBytes =
450
535
  Math.max(0, persistedBytesBefore - previousBytes) + bytes.byteLength;
@@ -463,7 +548,11 @@ export default defineEventHandler(async (event: H3Event) => {
463
548
  data: toBase64(bytes),
464
549
  createdAt: new Date().toISOString(),
465
550
  });
466
- bytesReceived = await sumRecordingChunkBytes(ownerEmail, recordingId);
551
+ bytesReceived = await sumRecordingChunkBytes(
552
+ ownerEmail,
553
+ recordingId,
554
+ uploadGenerationId,
555
+ );
467
556
  if (bytesReceived > MAX_RECORDING_UPLOAD_BYTES) {
468
557
  return failRecordingTooLarge(bytesReceived);
469
558
  }
@@ -487,6 +576,7 @@ export default defineEventHandler(async (event: H3Event) => {
487
576
  if (leaseFailure) return leaseFailure;
488
577
  await writeAppState(`recording-upload-${recordingId}`, {
489
578
  recordingId,
579
+ uploadGenerationId,
490
580
  status: isFinal ? "processing" : "uploading",
491
581
  progress,
492
582
  chunksReceived,
@@ -508,6 +598,7 @@ export default defineEventHandler(async (event: H3Event) => {
508
598
  if (leaseFailure) return leaseFailure;
509
599
  await writeAppState(`recording-upload-${recordingId}`, {
510
600
  recordingId,
601
+ uploadGenerationId,
511
602
  status: isFinal ? "processing" : "uploading",
512
603
  chunksReceived: Math.max(
513
604
  stateNumber(uploadState, "chunksReceived") ?? 0,
@@ -532,7 +623,11 @@ export default defineEventHandler(async (event: H3Event) => {
532
623
  if (isFinal) {
533
624
  const leaseFailure = await rejectIfLeaseLost();
534
625
  if (leaseFailure) return leaseFailure;
535
- bytesReceived = await sumRecordingChunkBytes(ownerEmail, recordingId);
626
+ bytesReceived = await sumRecordingChunkBytes(
627
+ ownerEmail,
628
+ recordingId,
629
+ uploadGenerationId,
630
+ );
536
631
  if (bytesReceived > MAX_RECORDING_UPLOAD_BYTES) {
537
632
  return failRecordingTooLarge(bytesReceived);
538
633
  }
@@ -541,7 +636,7 @@ export default defineEventHandler(async (event: H3Event) => {
541
636
  debugLog("[chunk] isFinal — invoking finalize", { recordingId });
542
637
  try {
543
638
  const result = await finalizeRecording.run(
544
- buildFinalizeArgs(recordingId, mimeType, query),
639
+ buildFinalizeArgs(recordingId, mimeType, query, uploadGenerationId),
545
640
  );
546
641
  debugLog("[chunk] finalize ok", {
547
642
  recordingId,
@@ -713,6 +808,7 @@ function buildFinalizeArgs(
713
808
  recordingId: string,
714
809
  mimeType: string,
715
810
  query: Record<string, unknown>,
811
+ uploadGenerationId: string | null,
716
812
  ) {
717
813
  const queryBoolean = (value: unknown): boolean | undefined => {
718
814
  if (value === undefined) return undefined;
@@ -729,6 +825,7 @@ function buildFinalizeArgs(
729
825
  hasCamera: queryBoolean(query.hasCamera),
730
826
  locallyTranscoded: queryBoolean(query.locallyTranscoded),
731
827
  mimeType,
828
+ ...(uploadGenerationId ? { uploadGenerationId } : {}),
732
829
  };
733
830
  }
734
831
 
@@ -743,6 +840,8 @@ async function handleResumableChunk(
743
840
  mimeType: string,
744
841
  query: Record<string, unknown>,
745
842
  ownerEmail: string,
843
+ attemptId: string | null,
844
+ uploadGenerationId: string | null,
746
845
  ) {
747
846
  const uploadProvider = await resolveResumableUploadProvider(
748
847
  session.providerId,
@@ -771,7 +870,10 @@ async function handleResumableChunk(
771
870
  error: "Cannot finalize an empty resumable upload",
772
871
  };
773
872
  }
774
- const lease = await renewUploadLease(recordingId);
873
+ const lease = await renewUploadLease(recordingId, {
874
+ attemptId,
875
+ generationId: uploadGenerationId,
876
+ });
775
877
  if (!lease.held) {
776
878
  setResponseStatus(event, 409);
777
879
  return {
@@ -798,7 +900,10 @@ async function handleResumableChunk(
798
900
  };
799
901
  }
800
902
  if (closeRes.updatedMeta) {
801
- const postCloseLease = await renewUploadLease(recordingId);
903
+ const postCloseLease = await renewUploadLease(recordingId, {
904
+ attemptId,
905
+ generationId: uploadGenerationId,
906
+ });
802
907
  if (!postCloseLease.held) {
803
908
  setResponseStatus(event, 409);
804
909
  return {
@@ -808,10 +913,14 @@ async function handleResumableChunk(
808
913
  "Recording upload has already failed.",
809
914
  };
810
915
  }
811
- await setResumableSession(recordingId, {
812
- ...session,
813
- meta: { ...session.meta, ...closeRes.updatedMeta },
814
- });
916
+ await setResumableSession(
917
+ recordingId,
918
+ {
919
+ ...session,
920
+ meta: { ...session.meta, ...closeRes.updatedMeta },
921
+ },
922
+ uploadGenerationId,
923
+ );
815
924
  }
816
925
  } else {
817
926
  // Idempotent replay guard: a client retry (after a lost response) can
@@ -835,7 +944,10 @@ async function handleResumableChunk(
835
944
  };
836
945
  }
837
946
  } else {
838
- const lease = await renewUploadLease(recordingId);
947
+ const lease = await renewUploadLease(recordingId, {
948
+ attemptId,
949
+ generationId: uploadGenerationId,
950
+ });
839
951
  if (!lease.held) {
840
952
  setResponseStatus(event, 409);
841
953
  return {
@@ -866,14 +978,25 @@ async function handleResumableChunk(
866
978
  ? putResult.ok && putResult.status !== 308
867
979
  : putResult.ok;
868
980
  if (!resultOk) {
869
- setResponseStatus(event, 502);
981
+ const restartRequired =
982
+ putResult.status === 404 || putResult.status === 410;
983
+ if (restartRequired) {
984
+ await deleteResumableSession(recordingId, uploadGenerationId).catch(
985
+ () => {},
986
+ );
987
+ }
988
+ setResponseStatus(event, restartRequired ? 409 : 502);
870
989
  return {
871
990
  ok: false,
872
991
  error: `Chunk upload failed (${putResult.status})`,
992
+ ...(restartRequired ? { restartRequired: true } : {}),
873
993
  };
874
994
  }
875
995
 
876
- const postUploadLease = await renewUploadLease(recordingId);
996
+ const postUploadLease = await renewUploadLease(recordingId, {
997
+ attemptId,
998
+ generationId: uploadGenerationId,
999
+ });
877
1000
  if (!postUploadLease.held) {
878
1001
  setResponseStatus(event, 409);
879
1002
  return {
@@ -883,14 +1006,18 @@ async function handleResumableChunk(
883
1006
  "Recording upload has already failed.",
884
1007
  };
885
1008
  }
886
- await setResumableSession(recordingId, {
887
- ...session,
888
- ...(putResult.updatedMeta
889
- ? { meta: { ...session.meta, ...putResult.updatedMeta } }
890
- : {}),
891
- bytesUploaded: start + bytes.byteLength,
892
- lastCommittedIndex: index,
893
- });
1009
+ await setResumableSession(
1010
+ recordingId,
1011
+ {
1012
+ ...session,
1013
+ ...(putResult.updatedMeta
1014
+ ? { meta: { ...session.meta, ...putResult.updatedMeta } }
1015
+ : {}),
1016
+ bytesUploaded: start + bytes.byteLength,
1017
+ lastCommittedIndex: index,
1018
+ },
1019
+ uploadGenerationId,
1020
+ );
894
1021
  finalizedSourceSizeBytes = start + bytes.byteLength;
895
1022
 
896
1023
  if (!isFinal) {
@@ -901,7 +1028,10 @@ async function handleResumableChunk(
901
1028
 
902
1029
  // isFinal — delegate to finalize-recording, which reads the resumable
903
1030
  // session and calls provider.resumable.completeSession.
904
- const finalLease = await renewUploadLease(recordingId);
1031
+ const finalLease = await renewUploadLease(recordingId, {
1032
+ attemptId,
1033
+ generationId: uploadGenerationId,
1034
+ });
905
1035
  if (!finalLease.held) {
906
1036
  setResponseStatus(event, 409);
907
1037
  return {
@@ -911,7 +1041,7 @@ async function handleResumableChunk(
911
1041
  }
912
1042
  try {
913
1043
  const result = await finalizeRecording.run(
914
- buildFinalizeArgs(recordingId, mimeType, query),
1044
+ buildFinalizeArgs(recordingId, mimeType, query, uploadGenerationId),
915
1045
  );
916
1046
  if ((result as any)?.status === "failed") {
917
1047
  setResponseStatus(event, 409);