@agent-native/core 0.134.1 → 0.135.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/corpus/README.md +3 -3
- package/corpus/core/CHANGELOG.md +113 -0
- package/corpus/core/package.json +3 -2
- package/corpus/core/src/automations/service.ts +41 -4
- package/corpus/core/src/cli/mcp-config-writers.ts +27 -21
- package/corpus/core/src/client/agent-page/AgentJobsTab.tsx +187 -85
- package/corpus/core/src/client/agent-page/AutomationDetailsDialog.tsx +208 -0
- package/corpus/core/src/client/agent-page/AutomationScheduleDialog.tsx +180 -0
- package/corpus/core/src/client/agent-page/TimezoneSelect.tsx +241 -0
- package/corpus/core/src/client/agent-page/use-jobs.ts +50 -10
- package/corpus/core/src/client/org/OrgSwitcher.tsx +26 -0
- package/corpus/core/src/client/settings/AccountSettingsCard.tsx +35 -31
- package/corpus/core/src/client/settings/SchedulingTimezoneField.tsx +71 -0
- package/corpus/core/src/client/settings/SettingsRow.tsx +113 -0
- package/corpus/core/src/client/settings/agent-settings-search.ts +2 -1
- package/corpus/core/src/client/settings/index.ts +6 -0
- package/corpus/core/src/integrations/identity.ts +2 -12
- package/corpus/core/src/jobs/actions/list-automation-runs.ts +33 -0
- package/corpus/core/src/jobs/actions/list-recurring-jobs.ts +27 -6
- package/corpus/core/src/jobs/actions/manage-recurring-job.ts +41 -9
- package/corpus/core/src/jobs/background-automation-runner.ts +88 -0
- package/corpus/core/src/jobs/cron.ts +52 -6
- package/corpus/core/src/jobs/frontmatter.ts +20 -0
- package/corpus/core/src/jobs/run-history.ts +242 -0
- package/corpus/core/src/jobs/scheduler.ts +73 -18
- package/corpus/core/src/jobs/tools.ts +54 -6
- package/corpus/core/src/localization/actions/get-localization-preference.ts +2 -2
- package/corpus/core/src/localization/actions/set-localization-preference.ts +49 -11
- package/corpus/core/src/localization/default-messages.ts +27 -0
- package/corpus/core/src/localization/shared.ts +39 -6
- package/corpus/core/src/localization/user-timezone.ts +36 -0
- package/corpus/core/src/org/app-roles-handlers.ts +1 -9
- package/corpus/core/src/org/index.ts +2 -0
- package/corpus/core/src/org/membership.ts +18 -0
- package/corpus/core/src/review/actions/create-review-comment.ts +4 -1
- package/corpus/core/src/review/actions/reply-review-comment.ts +4 -1
- package/corpus/core/src/review/index.ts +5 -0
- package/corpus/core/src/review/notifications.ts +163 -0
- package/corpus/core/src/review/types.ts +7 -0
- package/corpus/core/src/server/action-discovery.ts +4 -0
- package/corpus/core/src/server/activity-notifications.ts +174 -0
- package/corpus/core/src/server/app-name.ts +31 -0
- package/corpus/core/src/server/better-auth-instance.ts +4 -4
- package/corpus/core/src/server/email-templates.ts +70 -14
- package/corpus/core/src/server/email.ts +66 -3
- package/corpus/core/src/server/index.ts +10 -0
- package/corpus/core/src/sharing/access.ts +44 -2
- package/corpus/core/src/sharing/index.ts +5 -0
- package/corpus/core/src/sharing/recipients.ts +67 -0
- package/corpus/core/src/triggers/actions/list-automations.ts +27 -8
- package/corpus/core/src/triggers/actions/manage-automation.ts +17 -6
- package/corpus/core/src/triggers/actions.ts +16 -4
- package/corpus/core/src/triggers/dispatcher.ts +44 -30
- package/corpus/core/src/triggers/routes.ts +19 -6
- package/corpus/core/src/vitest-config.ts +68 -0
- package/corpus/core/vitest.config.ts +5 -0
- package/corpus/templates/analytics/app/pages/Settings.tsx +37 -53
- package/corpus/templates/analytics/changelog/2026-08-01-new-error-alerts-now-reach-you-by-email-and-a-scheduled-dash.md +6 -0
- package/corpus/templates/analytics/server/jobs/dashboard-report.ts +43 -0
- package/corpus/templates/analytics/server/lib/error-capture.ts +5 -1
- package/corpus/templates/analytics/vitest.config.ts +16 -12
- package/corpus/templates/assets/actions/refresh-generation-run.ts +3 -0
- package/corpus/templates/assets/app/components/ui/switch.tsx +1 -0
- package/corpus/templates/assets/app/hooks/use-assets-prefs.ts +55 -0
- package/corpus/templates/assets/app/i18n-data.ts +30 -0
- package/corpus/templates/assets/app/routes/settings.tsx +58 -55
- package/corpus/templates/assets/changelog/2026-08-01-get-an-email-when-a-generation-you-started-finishes-or-fails.md +6 -0
- package/corpus/templates/assets/server/lib/generation-run-notifications.ts +89 -0
- package/corpus/templates/assets/server/lib/video-runs.ts +8 -1
- package/corpus/templates/assets/server/routes/_agent-native/assets/user-prefs.get.ts +17 -0
- package/corpus/templates/assets/server/routes/_agent-native/assets/user-prefs.put.ts +36 -0
- package/corpus/templates/assets/shared/assets-user-prefs.ts +10 -0
- package/corpus/templates/assets/vitest.config.ts +16 -12
- package/corpus/templates/brain/app/routes/settings.tsx +15 -15
- package/corpus/templates/brain/vitest.config.ts +16 -12
- package/corpus/templates/calendar/app/pages/Settings.tsx +32 -40
- package/corpus/templates/calendar/changelog/2026-08-01-language-and-appearance-now-sit-together-in-one-preferences-.md +6 -0
- package/corpus/templates/calendar/vitest.config.ts +16 -12
- package/corpus/templates/chat/app/routes/settings.tsx +14 -22
- package/corpus/templates/chat/vitest.config.ts +9 -0
- package/corpus/templates/clips/.agents/skills/video-editing/SKILL.md +31 -0
- package/corpus/templates/clips/AGENTS.md +1 -0
- package/corpus/templates/clips/actions/add-comment.ts +12 -1
- package/corpus/templates/clips/actions/create-meeting.ts +2 -2
- package/corpus/templates/clips/actions/create-recording.ts +2 -2
- package/corpus/templates/clips/actions/finalize-recording.ts +42 -6
- package/corpus/templates/clips/actions/generate-filmstrip.ts +154 -0
- package/corpus/templates/clips/actions/get-recording-player-data.ts +6 -0
- package/corpus/templates/clips/actions/import-loom-recording.ts +2 -2
- package/corpus/templates/clips/actions/lib/ensure-recording-filmstrip.ts +258 -0
- package/corpus/templates/clips/actions/lib/ensure-seekable-video.ts +11 -3
- package/corpus/templates/clips/actions/react-to-comment.ts +13 -0
- package/corpus/templates/clips/actions/react-to-recording.ts +10 -1
- package/corpus/templates/clips/actions/reply-to-comment.ts +12 -1
- package/corpus/templates/clips/actions/stitch-recordings.ts +2 -2
- package/corpus/templates/clips/app/components/editor/editor-layout.tsx +152 -23
- package/corpus/templates/clips/app/components/editor/waveform.tsx +237 -75
- package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +23 -1
- package/corpus/templates/clips/app/components/settings/ai-setup-section.tsx +441 -0
- package/corpus/templates/clips/app/components/settings/slack-section.tsx +329 -0
- package/corpus/templates/clips/app/components/settings/types.ts +14 -0
- package/corpus/templates/clips/app/components/settings/video-storage-section.tsx +443 -0
- package/corpus/templates/clips/app/hooks/use-secret-status.ts +69 -0
- package/corpus/templates/clips/app/i18n/en-US.ts +9 -1
- package/corpus/templates/clips/app/lib/video-filmstrip.ts +262 -0
- package/corpus/templates/clips/app/routes/_app.settings._index.tsx +165 -1351
- package/corpus/templates/clips/changelog/2026-07-27-saved-clips-now-resume-from-the-last-uploaded-byte-after-an-.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-31-editor-timeline-now-shows-video-frames-behind-the-waveform-g.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-31-email-notifications-now-go-out-when-someone-comments-on-or-r.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-31-you-can-now-set-a-default-visibility-for-new-recordings-in-s.md +6 -0
- package/corpus/templates/clips/changelog/2026-08-01-email-notifications-for-comments-and-reactions-now-actually-.md +6 -0
- package/corpus/templates/clips/changelog/2026-08-03-the-desktop-app-now-opens-directly-beneath-its-tray-icon-on-.md +6 -0
- package/corpus/templates/clips/chrome-extension/package.json +1 -0
- package/corpus/templates/clips/chrome-extension/vitest.config.ts +9 -0
- package/corpus/templates/clips/desktop/package.json +1 -0
- package/corpus/templates/clips/desktop/src/app.tsx +17 -1
- package/corpus/templates/clips/desktop/src/lib/recorder.ts +331 -36
- package/corpus/templates/clips/desktop/src/lib/upload-recovery.ts +129 -0
- package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +815 -15
- package/corpus/templates/clips/desktop/src-tauri/src/tray.rs +61 -11
- package/corpus/templates/clips/desktop/vitest.config.ts +9 -0
- package/corpus/templates/clips/server/db/schema.ts +17 -0
- package/corpus/templates/clips/server/jobs/media-verification.ts +4 -0
- package/corpus/templates/clips/server/lib/activity-notifications.ts +185 -0
- package/corpus/templates/clips/server/lib/recording-media-cleanup.ts +2 -0
- package/corpus/templates/clips/server/lib/recording-upload-state.ts +46 -9
- package/corpus/templates/clips/server/lib/recordings.ts +24 -0
- package/corpus/templates/clips/server/lib/resumable-session.ts +10 -4
- package/corpus/templates/clips/server/lib/transactional-email-templates.ts +109 -0
- package/corpus/templates/clips/server/lib/upload-interruption.ts +8 -0
- package/corpus/templates/clips/server/lib/upload-lease.ts +50 -4
- package/corpus/templates/clips/server/lib/video-filmstrip-sprite.ts +207 -0
- package/corpus/templates/clips/server/lib/video-remux.ts +13 -4
- package/corpus/templates/clips/server/plugins/db.ts +16 -0
- package/corpus/templates/clips/server/routes/api/_agent-native-background/post-finalize-worker.post.ts +4 -0
- package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/abort.post.ts +123 -36
- package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/chunk.post.ts +159 -29
- package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/interrupt.post.ts +197 -0
- package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/reset-chunks.post.ts +193 -39
- package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/resume.get.ts +191 -12
- package/corpus/templates/clips/shared/clips-ai-prefs.ts +6 -0
- package/corpus/templates/clips/shared/feature-flags.ts +12 -0
- package/corpus/templates/clips/shared/recording-core.ts +4 -0
- package/corpus/templates/clips/vitest.config.ts +23 -19
- package/corpus/templates/content/actions/_database-utils.ts +113 -4
- package/corpus/templates/content/actions/add-comment.ts +15 -1
- package/corpus/templates/content/actions/get-document.ts +84 -3
- package/corpus/templates/content/actions/list-documents.ts +1 -0
- package/corpus/templates/content/app/components/editor/BuilderBodySyncingNotice.tsx +9 -2
- package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +34 -15
- package/corpus/templates/content/app/components/editor/DocumentToolbar.tsx +55 -13
- package/corpus/templates/content/app/components/editor/body-hydration.ts +12 -6
- package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +2 -3
- package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +5 -12
- package/corpus/templates/content/app/components/ui/switch.tsx +1 -0
- package/corpus/templates/content/app/hooks/use-content-database.ts +16 -27
- package/corpus/templates/content/app/hooks/use-content-prefs.ts +55 -0
- package/corpus/templates/content/app/hooks/use-create-page.ts +3 -6
- package/corpus/templates/content/app/hooks/use-document-properties.ts +9 -16
- package/corpus/templates/content/app/hooks/use-document-versions.ts +3 -3
- package/corpus/templates/content/app/hooks/use-documents.ts +85 -29
- package/corpus/templates/content/app/hooks/use-notion.ts +15 -13
- package/corpus/templates/content/app/i18n-data.ts +72 -1
- package/corpus/templates/content/app/lib/document-query.ts +36 -0
- package/corpus/templates/content/app/routes/_app.settings.tsx +47 -18
- package/corpus/templates/content/changelog/2026-07-30-breadcrumb-menus-now-hide-internal-system-pages.md +6 -0
- package/corpus/templates/content/changelog/2026-08-01-get-an-email-when-someone-comments-on-replies-in-or-mentions.md +6 -0
- package/corpus/templates/content/changelog/2026-08-02-pages-opened-from-a-database-now-keep-that-database-s-fields.md +6 -0
- package/corpus/templates/content/server/lib/comment-notifications.ts +146 -0
- package/corpus/templates/content/server/lib/document-context.ts +11 -6
- package/corpus/templates/content/server/routes/_agent-native/content/user-prefs.get.ts +17 -0
- package/corpus/templates/content/server/routes/_agent-native/content/user-prefs.put.ts +36 -0
- package/corpus/templates/content/shared/api.ts +8 -0
- package/corpus/templates/content/shared/content-user-prefs.ts +11 -0
- package/corpus/templates/content/vitest.config.ts +20 -16
- package/corpus/templates/crm/app/routes/settings.tsx +14 -22
- package/corpus/templates/crm/vitest.config.ts +8 -4
- package/corpus/templates/design/app/routes/settings.tsx +14 -22
- package/corpus/templates/design/changelog/2026-08-01-review-comments-replies-and-mentions-now-send-an-email-to-th.md +6 -0
- package/corpus/templates/design/vitest.config.ts +18 -12
- package/corpus/templates/dispatch/app/routes/settings.tsx +26 -40
- package/corpus/templates/dispatch/vitest.config.ts +11 -7
- package/corpus/templates/factory/app/routes/settings.tsx +14 -22
- package/corpus/templates/factory/vitest.config.ts +9 -0
- package/corpus/templates/forms/app/routes/_app.settings.tsx +14 -22
- package/corpus/templates/forms/vitest.config.ts +27 -23
- package/corpus/templates/macros/app/routes/settings.tsx +14 -22
- package/corpus/templates/macros/vitest.config.ts +16 -12
- package/corpus/templates/mail/app/pages/SettingsPage.tsx +14 -16
- package/corpus/templates/mail/vitest.config.ts +16 -12
- package/corpus/templates/plan/app/routes/settings.tsx +30 -44
- package/corpus/templates/plan/vitest.config.ts +24 -20
- package/corpus/templates/slides/actions/add-slide-comment.ts +12 -1
- package/corpus/templates/slides/app/components/ui/switch.tsx +1 -0
- package/corpus/templates/slides/app/hooks/use-slides-prefs.ts +55 -0
- package/corpus/templates/slides/app/i18n/en-US.ts +4 -0
- package/corpus/templates/slides/app/routes/settings.tsx +45 -22
- package/corpus/templates/slides/changelog/2026-08-01-get-an-email-when-someone-comments-on-or-replies-in-your-dec.md +6 -0
- package/corpus/templates/slides/server/lib/comment-notifications.ts +182 -0
- package/corpus/templates/slides/server/routes/_agent-native/slides/user-prefs.get.ts +16 -0
- package/corpus/templates/slides/server/routes/_agent-native/slides/user-prefs.put.ts +36 -0
- package/corpus/templates/slides/shared/slides-user-prefs.ts +11 -0
- package/corpus/templates/slides/vitest.config.ts +17 -13
- package/corpus/templates/tasks/app/components/layout/Sidebar.tsx +2 -0
- package/corpus/templates/tasks/app/routes/settings.tsx +14 -22
- package/corpus/templates/tasks/vitest.config.ts +17 -13
- package/corpus/toolkit/CHANGELOG.md +9 -0
- package/corpus/toolkit/package.json +1 -1
- package/corpus/toolkit/vitest.config.ts +3 -0
- package/dist/automations/service.d.ts +2 -0
- package/dist/automations/service.d.ts.map +1 -1
- package/dist/automations/service.js +29 -4
- package/dist/automations/service.js.map +1 -1
- package/dist/cli/mcp-config-writers.d.ts.map +1 -1
- package/dist/cli/mcp-config-writers.js +24 -23
- package/dist/cli/mcp-config-writers.js.map +1 -1
- package/dist/client/agent-page/AgentJobsTab.d.ts.map +1 -1
- package/dist/client/agent-page/AgentJobsTab.js +86 -29
- package/dist/client/agent-page/AgentJobsTab.js.map +1 -1
- package/dist/client/agent-page/AutomationDetailsDialog.d.ts +21 -0
- package/dist/client/agent-page/AutomationDetailsDialog.d.ts.map +1 -0
- package/dist/client/agent-page/AutomationDetailsDialog.js +33 -0
- package/dist/client/agent-page/AutomationDetailsDialog.js.map +1 -0
- package/dist/client/agent-page/AutomationScheduleDialog.d.ts +15 -0
- package/dist/client/agent-page/AutomationScheduleDialog.d.ts.map +1 -0
- package/dist/client/agent-page/AutomationScheduleDialog.js +46 -0
- package/dist/client/agent-page/AutomationScheduleDialog.js.map +1 -0
- package/dist/client/agent-page/TimezoneSelect.d.ts +17 -0
- package/dist/client/agent-page/TimezoneSelect.d.ts.map +1 -0
- package/dist/client/agent-page/TimezoneSelect.js +107 -0
- package/dist/client/agent-page/TimezoneSelect.js.map +1 -0
- package/dist/client/agent-page/use-jobs.d.ts +20 -7
- package/dist/client/agent-page/use-jobs.d.ts.map +1 -1
- package/dist/client/agent-page/use-jobs.js +24 -4
- package/dist/client/agent-page/use-jobs.js.map +1 -1
- package/dist/client/org/OrgSwitcher.d.ts +8 -1
- package/dist/client/org/OrgSwitcher.d.ts.map +1 -1
- package/dist/client/org/OrgSwitcher.js +8 -2
- package/dist/client/org/OrgSwitcher.js.map +1 -1
- package/dist/client/settings/AccountSettingsCard.d.ts.map +1 -1
- package/dist/client/settings/AccountSettingsCard.js +10 -9
- package/dist/client/settings/AccountSettingsCard.js.map +1 -1
- package/dist/client/settings/SchedulingTimezoneField.d.ts +10 -0
- package/dist/client/settings/SchedulingTimezoneField.d.ts.map +1 -0
- package/dist/client/settings/SchedulingTimezoneField.js +30 -0
- package/dist/client/settings/SchedulingTimezoneField.js.map +1 -0
- package/dist/client/settings/SettingsRow.d.ts +36 -0
- package/dist/client/settings/SettingsRow.d.ts.map +1 -0
- package/dist/client/settings/SettingsRow.js +15 -0
- package/dist/client/settings/SettingsRow.js.map +1 -0
- package/dist/client/settings/agent-settings-search.d.ts.map +1 -1
- package/dist/client/settings/agent-settings-search.js +1 -1
- package/dist/client/settings/agent-settings-search.js.map +1 -1
- package/dist/client/settings/index.d.ts +1 -0
- package/dist/client/settings/index.d.ts.map +1 -1
- package/dist/client/settings/index.js +1 -0
- package/dist/client/settings/index.js.map +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +1 -1
- package/dist/integrations/identity.d.ts.map +1 -1
- package/dist/integrations/identity.js +2 -11
- package/dist/integrations/identity.js.map +1 -1
- package/dist/jobs/actions/list-automation-runs.d.ts +8 -0
- package/dist/jobs/actions/list-automation-runs.d.ts.map +1 -0
- package/dist/jobs/actions/list-automation-runs.js +29 -0
- package/dist/jobs/actions/list-automation-runs.js.map +1 -0
- package/dist/jobs/actions/list-recurring-jobs.d.ts +2 -0
- package/dist/jobs/actions/list-recurring-jobs.d.ts.map +1 -1
- package/dist/jobs/actions/list-recurring-jobs.js +20 -7
- package/dist/jobs/actions/list-recurring-jobs.js.map +1 -1
- package/dist/jobs/actions/manage-recurring-job.d.ts +2 -0
- package/dist/jobs/actions/manage-recurring-job.js +30 -9
- package/dist/jobs/actions/manage-recurring-job.js.map +1 -1
- package/dist/jobs/background-automation-runner.d.ts.map +1 -1
- package/dist/jobs/background-automation-runner.js +60 -0
- package/dist/jobs/background-automation-runner.js.map +1 -1
- package/dist/jobs/cron.d.ts +16 -2
- package/dist/jobs/cron.d.ts.map +1 -1
- package/dist/jobs/cron.js +43 -6
- package/dist/jobs/cron.js.map +1 -1
- package/dist/jobs/frontmatter.d.ts +12 -0
- package/dist/jobs/frontmatter.d.ts.map +1 -1
- package/dist/jobs/frontmatter.js +8 -0
- package/dist/jobs/frontmatter.js.map +1 -1
- package/dist/jobs/run-history.d.ts +56 -0
- package/dist/jobs/run-history.d.ts.map +1 -0
- package/dist/jobs/run-history.js +169 -0
- package/dist/jobs/run-history.js.map +1 -0
- package/dist/jobs/scheduler.d.ts.map +1 -1
- package/dist/jobs/scheduler.js +51 -18
- package/dist/jobs/scheduler.js.map +1 -1
- package/dist/jobs/tools.d.ts.map +1 -1
- package/dist/jobs/tools.js +37 -6
- package/dist/jobs/tools.js.map +1 -1
- package/dist/localization/actions/get-localization-preference.d.ts +1 -2
- package/dist/localization/actions/get-localization-preference.d.ts.map +1 -1
- package/dist/localization/actions/get-localization-preference.js.map +1 -1
- package/dist/localization/actions/set-localization-preference.d.ts +3 -3
- package/dist/localization/actions/set-localization-preference.d.ts.map +1 -1
- package/dist/localization/actions/set-localization-preference.js +33 -8
- package/dist/localization/actions/set-localization-preference.js.map +1 -1
- package/dist/localization/default-messages.d.ts +25 -0
- package/dist/localization/default-messages.d.ts.map +1 -1
- package/dist/localization/default-messages.js +25 -0
- package/dist/localization/default-messages.js.map +1 -1
- package/dist/localization/shared.d.ts +16 -1
- package/dist/localization/shared.d.ts.map +1 -1
- package/dist/localization/shared.js +22 -4
- package/dist/localization/shared.js.map +1 -1
- package/dist/localization/user-timezone.d.ts +11 -0
- package/dist/localization/user-timezone.d.ts.map +1 -0
- package/dist/localization/user-timezone.js +31 -0
- package/dist/localization/user-timezone.js.map +1 -0
- package/dist/notifications/routes.d.ts +4 -4
- package/dist/observability/routes.d.ts +3 -3
- package/dist/org/app-roles-handlers.d.ts.map +1 -1
- package/dist/org/app-roles-handlers.js +1 -8
- package/dist/org/app-roles-handlers.js.map +1 -1
- package/dist/org/index.d.ts +1 -0
- package/dist/org/index.d.ts.map +1 -1
- package/dist/org/index.js +1 -0
- package/dist/org/index.js.map +1 -1
- package/dist/org/membership.d.ts +6 -0
- package/dist/org/membership.d.ts.map +1 -0
- package/dist/org/membership.js +16 -0
- package/dist/org/membership.js.map +1 -0
- package/dist/progress/routes.d.ts +1 -1
- package/dist/provider-api/actions/custom-provider-registration.d.ts +13 -13
- package/dist/provider-api/actions/provider-api.d.ts +8 -8
- package/dist/provider-api/corpus-jobs.d.ts +2 -2
- package/dist/review/actions/create-review-comment.d.ts +32 -1
- package/dist/review/actions/create-review-comment.d.ts.map +1 -1
- package/dist/review/actions/create-review-comment.js +3 -1
- package/dist/review/actions/create-review-comment.js.map +1 -1
- package/dist/review/actions/reply-review-comment.d.ts +32 -1
- package/dist/review/actions/reply-review-comment.d.ts.map +1 -1
- package/dist/review/actions/reply-review-comment.js +3 -1
- package/dist/review/actions/reply-review-comment.js.map +1 -1
- package/dist/review/index.d.ts +1 -0
- package/dist/review/index.d.ts.map +1 -1
- package/dist/review/index.js +1 -0
- package/dist/review/index.js.map +1 -1
- package/dist/review/notifications.d.ts +24 -0
- package/dist/review/notifications.d.ts.map +1 -0
- package/dist/review/notifications.js +123 -0
- package/dist/review/notifications.js.map +1 -0
- package/dist/review/types.d.ts +5 -0
- package/dist/review/types.d.ts.map +1 -1
- package/dist/review/types.js.map +1 -1
- package/dist/server/action-discovery.d.ts.map +1 -1
- package/dist/server/action-discovery.js +4 -0
- package/dist/server/action-discovery.js.map +1 -1
- package/dist/server/activity-notifications.d.ts +61 -0
- package/dist/server/activity-notifications.d.ts.map +1 -0
- package/dist/server/activity-notifications.js +103 -0
- package/dist/server/activity-notifications.js.map +1 -0
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/app-name.d.ts +18 -0
- package/dist/server/app-name.d.ts.map +1 -1
- package/dist/server/app-name.js +31 -0
- package/dist/server/app-name.js.map +1 -1
- package/dist/server/better-auth-instance.js +4 -4
- package/dist/server/better-auth-instance.js.map +1 -1
- package/dist/server/email-templates.d.ts +12 -0
- package/dist/server/email-templates.d.ts.map +1 -1
- package/dist/server/email-templates.js +57 -14
- package/dist/server/email-templates.js.map +1 -1
- package/dist/server/email.d.ts +12 -0
- package/dist/server/email.d.ts.map +1 -1
- package/dist/server/email.js +52 -5
- package/dist/server/email.js.map +1 -1
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +1 -0
- package/dist/server/index.js.map +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/dist/sharing/access.d.ts.map +1 -1
- package/dist/sharing/access.js +32 -2
- package/dist/sharing/access.js.map +1 -1
- package/dist/sharing/index.d.ts +1 -0
- package/dist/sharing/index.d.ts.map +1 -1
- package/dist/sharing/index.js +1 -0
- package/dist/sharing/index.js.map +1 -1
- package/dist/sharing/recipients.d.ts +31 -0
- package/dist/sharing/recipients.d.ts.map +1 -0
- package/dist/sharing/recipients.js +40 -0
- package/dist/sharing/recipients.js.map +1 -0
- package/dist/templates/chat/app/routes/settings.tsx +14 -22
- package/dist/templates/chat/vitest.config.ts +9 -0
- package/dist/triggers/actions/list-automations.d.ts +2 -0
- package/dist/triggers/actions/list-automations.d.ts.map +1 -1
- package/dist/triggers/actions/list-automations.js +19 -8
- package/dist/triggers/actions/list-automations.js.map +1 -1
- package/dist/triggers/actions/manage-automation.d.ts +6 -0
- package/dist/triggers/actions/manage-automation.js +12 -6
- package/dist/triggers/actions/manage-automation.js.map +1 -1
- package/dist/triggers/actions.d.ts.map +1 -1
- package/dist/triggers/actions.js +15 -4
- package/dist/triggers/actions.js.map +1 -1
- package/dist/triggers/dispatcher.d.ts.map +1 -1
- package/dist/triggers/dispatcher.js +19 -17
- package/dist/triggers/dispatcher.js.map +1 -1
- package/dist/triggers/routes.d.ts.map +1 -1
- package/dist/triggers/routes.js +6 -6
- package/dist/triggers/routes.js.map +1 -1
- package/dist/vitest-config.d.ts +10 -0
- package/dist/vitest-config.d.ts.map +1 -0
- package/dist/vitest-config.js +55 -0
- package/dist/vitest-config.js.map +1 -0
- package/package.json +5 -4
- package/src/automations/service.ts +41 -4
- package/src/cli/mcp-config-writers.ts +27 -21
- package/src/client/agent-page/AgentJobsTab.tsx +187 -85
- package/src/client/agent-page/AutomationDetailsDialog.tsx +208 -0
- package/src/client/agent-page/AutomationScheduleDialog.tsx +180 -0
- package/src/client/agent-page/TimezoneSelect.tsx +241 -0
- package/src/client/agent-page/use-jobs.ts +50 -10
- package/src/client/org/OrgSwitcher.tsx +26 -0
- package/src/client/settings/AccountSettingsCard.tsx +35 -31
- package/src/client/settings/SchedulingTimezoneField.tsx +71 -0
- package/src/client/settings/SettingsRow.tsx +113 -0
- package/src/client/settings/agent-settings-search.ts +2 -1
- package/src/client/settings/index.ts +6 -0
- package/src/integrations/identity.ts +2 -12
- package/src/jobs/actions/list-automation-runs.ts +33 -0
- package/src/jobs/actions/list-recurring-jobs.ts +27 -6
- package/src/jobs/actions/manage-recurring-job.ts +41 -9
- package/src/jobs/background-automation-runner.ts +88 -0
- package/src/jobs/cron.ts +52 -6
- package/src/jobs/frontmatter.ts +20 -0
- package/src/jobs/run-history.ts +242 -0
- package/src/jobs/scheduler.ts +73 -18
- package/src/jobs/tools.ts +54 -6
- package/src/localization/actions/get-localization-preference.ts +2 -2
- package/src/localization/actions/set-localization-preference.ts +49 -11
- package/src/localization/default-messages.ts +27 -0
- package/src/localization/shared.ts +39 -6
- package/src/localization/user-timezone.ts +36 -0
- package/src/org/app-roles-handlers.ts +1 -9
- package/src/org/index.ts +2 -0
- package/src/org/membership.ts +18 -0
- package/src/review/actions/create-review-comment.ts +4 -1
- package/src/review/actions/reply-review-comment.ts +4 -1
- package/src/review/index.ts +5 -0
- package/src/review/notifications.ts +163 -0
- package/src/review/types.ts +7 -0
- package/src/server/action-discovery.ts +4 -0
- package/src/server/activity-notifications.ts +174 -0
- package/src/server/app-name.ts +31 -0
- package/src/server/better-auth-instance.ts +4 -4
- package/src/server/email-templates.ts +70 -14
- package/src/server/email.ts +66 -3
- package/src/server/index.ts +10 -0
- package/src/sharing/access.ts +44 -2
- package/src/sharing/index.ts +5 -0
- package/src/sharing/recipients.ts +67 -0
- package/src/templates/chat/app/routes/settings.tsx +14 -22
- package/src/templates/chat/vitest.config.ts +9 -0
- package/src/triggers/actions/list-automations.ts +27 -8
- package/src/triggers/actions/manage-automation.ts +17 -6
- package/src/triggers/actions.ts +16 -4
- package/src/triggers/dispatcher.ts +44 -30
- package/src/triggers/routes.ts +19 -6
- package/src/vitest-config.ts +68 -0
|
@@ -24,15 +24,19 @@
|
|
|
24
24
|
* Route: POST /api/uploads/:recordingId/reset-chunks
|
|
25
25
|
*/
|
|
26
26
|
|
|
27
|
+
import { randomUUID } from "node:crypto";
|
|
28
|
+
|
|
27
29
|
import {
|
|
30
|
+
compareAndSetAppState,
|
|
31
|
+
readAppState,
|
|
28
32
|
writeAppState,
|
|
29
|
-
deleteAppStateByPrefix,
|
|
30
33
|
} from "@agent-native/core/application-state";
|
|
34
|
+
import { isFeatureFlagEnabled } from "@agent-native/core/feature-flags";
|
|
31
35
|
import { getActiveFileUploadProviderForRequest } from "@agent-native/core/file-upload";
|
|
32
36
|
import { runWithRequestContext } from "@agent-native/core/server";
|
|
33
37
|
import type { UploadMode } from "@shared/recording-core.js";
|
|
34
38
|
import { MAX_UPLOAD_BYTES as MAX_RECORDING_UPLOAD_BYTES } from "@shared/upload-limits.js";
|
|
35
|
-
import { and, eq } from "drizzle-orm";
|
|
39
|
+
import { and, eq, isNull } from "drizzle-orm";
|
|
36
40
|
import {
|
|
37
41
|
defineEventHandler,
|
|
38
42
|
getRouterParam,
|
|
@@ -41,8 +45,10 @@ import {
|
|
|
41
45
|
type H3Event,
|
|
42
46
|
} from "h3";
|
|
43
47
|
|
|
48
|
+
import { UPLOAD_RETRY_RESUME_FLAG } from "../../../../../shared/feature-flags.js";
|
|
44
49
|
import { getDb, schema } from "../../../../db/index.js";
|
|
45
50
|
import { isMediaVerificationPending } from "../../../../lib/media-verification-state.js";
|
|
51
|
+
import { deleteRecordingChunks } from "../../../../lib/recording-upload-state.js";
|
|
46
52
|
import {
|
|
47
53
|
getEventOwnerContext,
|
|
48
54
|
ownerEmailMatches,
|
|
@@ -52,7 +58,10 @@ import {
|
|
|
52
58
|
setResumableSession,
|
|
53
59
|
} from "../../../../lib/resumable-session.js";
|
|
54
60
|
import { shouldEnableStreamingUpload } from "../../../../lib/streaming-upload-mode.js";
|
|
55
|
-
import {
|
|
61
|
+
import {
|
|
62
|
+
renewUploadLease,
|
|
63
|
+
uploadLeaseExpiry,
|
|
64
|
+
} from "../../../../lib/upload-lease.js";
|
|
56
65
|
import { allowsSqlRecordingChunkScratch } from "../../../../lib/video-storage.js";
|
|
57
66
|
|
|
58
67
|
interface CompressionMeta {
|
|
@@ -98,7 +107,15 @@ export default defineEventHandler(async (event: H3Event) => {
|
|
|
98
107
|
compression?: CompressionMeta | null;
|
|
99
108
|
requestStreaming?: boolean;
|
|
100
109
|
mimeType?: string;
|
|
110
|
+
attemptId?: string;
|
|
111
|
+
uploadGenerationId?: string;
|
|
112
|
+
useGenerationFence?: boolean;
|
|
101
113
|
} | null;
|
|
114
|
+
const recoveryEnabled = await isFeatureFlagEnabled(UPLOAD_RETRY_RESUME_FLAG, {
|
|
115
|
+
userEmail: ownerEmail,
|
|
116
|
+
userKey: ownerEmail,
|
|
117
|
+
orgId,
|
|
118
|
+
});
|
|
102
119
|
|
|
103
120
|
// Sanitize compression metadata. The recorder is the only client we trust
|
|
104
121
|
// here, but the values land in Sentry extras — so we still bound them to
|
|
@@ -121,6 +138,8 @@ export default defineEventHandler(async (event: H3Event) => {
|
|
|
121
138
|
id: schema.recordings.id,
|
|
122
139
|
status: schema.recordings.status,
|
|
123
140
|
videoUrl: schema.recordings.videoUrl,
|
|
141
|
+
uploadAttemptId: schema.recordings.uploadAttemptId,
|
|
142
|
+
uploadGenerationId: schema.recordings.uploadGenerationId,
|
|
124
143
|
})
|
|
125
144
|
.from(schema.recordings)
|
|
126
145
|
.where(
|
|
@@ -140,6 +159,35 @@ export default defineEventHandler(async (event: H3Event) => {
|
|
|
140
159
|
return { error: "Recording is already ready" };
|
|
141
160
|
}
|
|
142
161
|
|
|
162
|
+
const requestedAttemptId =
|
|
163
|
+
typeof body?.attemptId === "string" &&
|
|
164
|
+
body.attemptId.length > 0 &&
|
|
165
|
+
body.attemptId.length <= 128
|
|
166
|
+
? body.attemptId
|
|
167
|
+
: null;
|
|
168
|
+
const existingAttemptId = existing.uploadAttemptId ?? null;
|
|
169
|
+
const existingGenerationId = existing.uploadGenerationId ?? null;
|
|
170
|
+
if (recoveryEnabled && existingAttemptId !== requestedAttemptId) {
|
|
171
|
+
setResponseStatus(event, 409);
|
|
172
|
+
return {
|
|
173
|
+
error: "A newer upload retry is already active.",
|
|
174
|
+
staleAttempt: true,
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
const requestedGenerationId =
|
|
178
|
+
typeof body?.uploadGenerationId === "string" &&
|
|
179
|
+
body.uploadGenerationId.length > 0 &&
|
|
180
|
+
body.uploadGenerationId.length <= 128
|
|
181
|
+
? body.uploadGenerationId
|
|
182
|
+
: null;
|
|
183
|
+
if (recoveryEnabled && existingGenerationId !== requestedGenerationId) {
|
|
184
|
+
setResponseStatus(event, 409);
|
|
185
|
+
return {
|
|
186
|
+
error: "A newer upload generation is already active.",
|
|
187
|
+
staleAttempt: true,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
143
191
|
if (
|
|
144
192
|
await isMediaVerificationPending({
|
|
145
193
|
ownerEmail,
|
|
@@ -150,15 +198,72 @@ export default defineEventHandler(async (event: H3Event) => {
|
|
|
150
198
|
setResponseStatus(event, 409);
|
|
151
199
|
return { error: "Recording is still being verified" };
|
|
152
200
|
}
|
|
201
|
+
if (existing.status !== "uploading" && existing.status !== "failed") {
|
|
202
|
+
setResponseStatus(event, 409);
|
|
203
|
+
return { error: "Recording upload is no longer resettable" };
|
|
204
|
+
}
|
|
153
205
|
|
|
154
|
-
|
|
155
|
-
|
|
206
|
+
// Fence this reset before deleting any provider or buffered state. A
|
|
207
|
+
// retry that lost the token race must not tear down the winner's session.
|
|
208
|
+
const now = new Date().toISOString();
|
|
209
|
+
// Only clients that can carry the returned generation may opt into the
|
|
210
|
+
// fence. Retry claims always opt in; legacy reset callers keep the null
|
|
211
|
+
// generation wire contract until they are upgraded.
|
|
212
|
+
const useGenerationFence =
|
|
213
|
+
recoveryEnabled &&
|
|
214
|
+
(requestedAttemptId !== null ||
|
|
215
|
+
requestedGenerationId !== null ||
|
|
216
|
+
body?.useGenerationFence === true);
|
|
217
|
+
const nextGenerationId = useGenerationFence ? randomUUID() : null;
|
|
218
|
+
const uploadStateKey = `recording-upload-${recordingId}`;
|
|
219
|
+
const uploadStateSnapshot = await readAppState(uploadStateKey);
|
|
220
|
+
const reset = await db
|
|
221
|
+
.update(schema.recordings)
|
|
222
|
+
.set({
|
|
223
|
+
status: "uploading",
|
|
224
|
+
failureReason: null,
|
|
225
|
+
uploadProgress: 0,
|
|
226
|
+
uploadGenerationId: nextGenerationId,
|
|
227
|
+
...(!recoveryEnabled ? { uploadAttemptId: null } : {}),
|
|
228
|
+
uploadLeaseExpiresAt: uploadLeaseExpiry(),
|
|
229
|
+
updatedAt: now,
|
|
230
|
+
})
|
|
231
|
+
.where(
|
|
232
|
+
and(
|
|
233
|
+
eq(schema.recordings.id, recordingId),
|
|
234
|
+
ownerEmailMatches(schema.recordings.ownerEmail, ownerEmail),
|
|
235
|
+
eq(schema.recordings.status, existing.status),
|
|
236
|
+
existingAttemptId === null
|
|
237
|
+
? isNull(schema.recordings.uploadAttemptId)
|
|
238
|
+
: eq(schema.recordings.uploadAttemptId, existingAttemptId),
|
|
239
|
+
existingGenerationId === null
|
|
240
|
+
? isNull(schema.recordings.uploadGenerationId)
|
|
241
|
+
: eq(schema.recordings.uploadGenerationId, existingGenerationId),
|
|
242
|
+
),
|
|
243
|
+
)
|
|
244
|
+
.returning({ id: schema.recordings.id });
|
|
245
|
+
|
|
246
|
+
if (reset.length !== 1) {
|
|
247
|
+
setResponseStatus(event, 409);
|
|
248
|
+
return {
|
|
249
|
+
error: "A newer upload retry is already active.",
|
|
250
|
+
staleAttempt: true,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const cleared = await deleteRecordingChunks(
|
|
255
|
+
ownerEmail,
|
|
256
|
+
recordingId,
|
|
257
|
+
existingGenerationId,
|
|
156
258
|
);
|
|
157
259
|
// Clear any stale resumable session so a buffered retry does not
|
|
158
260
|
// accidentally route through handleResumableChunk with stale offsets.
|
|
159
|
-
await deleteResumableSession(recordingId).catch(
|
|
261
|
+
await deleteResumableSession(recordingId, existingGenerationId).catch(
|
|
262
|
+
() => {},
|
|
263
|
+
);
|
|
160
264
|
|
|
161
265
|
let uploadMode: UploadMode = "buffered";
|
|
266
|
+
let compensateStartedSession: (() => Promise<void>) | null = null;
|
|
162
267
|
if (body?.requestStreaming === true) {
|
|
163
268
|
const mimeType = normalizeVideoMimeType(body.mimeType);
|
|
164
269
|
if (!mimeType) {
|
|
@@ -184,17 +289,33 @@ export default defineEventHandler(async (event: H3Event) => {
|
|
|
184
289
|
mimeType,
|
|
185
290
|
MAX_RECORDING_UPLOAD_BYTES,
|
|
186
291
|
);
|
|
187
|
-
await setResumableSession(
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
292
|
+
await setResumableSession(
|
|
293
|
+
recordingId,
|
|
294
|
+
{
|
|
295
|
+
providerId: uploadProvider.id,
|
|
296
|
+
sessionId: session.sessionId,
|
|
297
|
+
meta: {
|
|
298
|
+
...session.meta,
|
|
299
|
+
stableUrl: true,
|
|
300
|
+
recordAsset: false,
|
|
301
|
+
},
|
|
302
|
+
bytesUploaded: 0,
|
|
303
|
+
lastCommittedIndex: -1,
|
|
194
304
|
},
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
305
|
+
nextGenerationId,
|
|
306
|
+
);
|
|
307
|
+
compensateStartedSession = async () => {
|
|
308
|
+
if (!uploadProvider.resumable?.abortSession) {
|
|
309
|
+
throw new Error(
|
|
310
|
+
`Resumable upload provider ${uploadProvider.id} cannot abort the discarded retry session`,
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
await uploadProvider.resumable.abortSession({
|
|
314
|
+
sessionId: session.sessionId,
|
|
315
|
+
meta: session.meta,
|
|
316
|
+
});
|
|
317
|
+
await deleteResumableSession(recordingId, nextGenerationId);
|
|
318
|
+
};
|
|
198
319
|
uploadMode = "streaming";
|
|
199
320
|
} catch (err) {
|
|
200
321
|
if (!bufferedFallbackAvailable) {
|
|
@@ -219,19 +340,64 @@ export default defineEventHandler(async (event: H3Event) => {
|
|
|
219
340
|
}
|
|
220
341
|
}
|
|
221
342
|
|
|
343
|
+
const resetLease = await renewUploadLease(recordingId, {
|
|
344
|
+
attemptId: recoveryEnabled ? existingAttemptId : null,
|
|
345
|
+
generationId: nextGenerationId,
|
|
346
|
+
});
|
|
347
|
+
if (!resetLease.held) {
|
|
348
|
+
await compensateStartedSession?.();
|
|
349
|
+
setResponseStatus(event, 409);
|
|
350
|
+
return {
|
|
351
|
+
error: "Recording upload changed while its retry was starting.",
|
|
352
|
+
staleAttempt: true,
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
|
|
222
356
|
// Reset the per-recording upload progress so the UI poller sees the
|
|
223
357
|
// re-upload restart from 0 and doesn't briefly show "100% then
|
|
224
358
|
// re-running" on the post-compression chunked upload pass.
|
|
225
|
-
const
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
359
|
+
const uploadStateUpdated = await compareAndSetAppState(
|
|
360
|
+
uploadStateKey,
|
|
361
|
+
uploadStateSnapshot,
|
|
362
|
+
{
|
|
363
|
+
recordingId,
|
|
364
|
+
status: "uploading",
|
|
365
|
+
progress: 0,
|
|
366
|
+
chunksReceived: 0,
|
|
367
|
+
bytesReceived: 0,
|
|
368
|
+
uploadAttemptId: recoveryEnabled ? existingAttemptId : null,
|
|
369
|
+
uploadGenerationId: nextGenerationId,
|
|
370
|
+
maxBytes: MAX_RECORDING_UPLOAD_BYTES,
|
|
371
|
+
updatedAt: now,
|
|
372
|
+
},
|
|
373
|
+
);
|
|
374
|
+
if (!uploadStateUpdated) {
|
|
375
|
+
const [current] = await db
|
|
376
|
+
.select({
|
|
377
|
+
status: schema.recordings.status,
|
|
378
|
+
uploadAttemptId: schema.recordings.uploadAttemptId,
|
|
379
|
+
uploadGenerationId: schema.recordings.uploadGenerationId,
|
|
380
|
+
})
|
|
381
|
+
.from(schema.recordings)
|
|
382
|
+
.where(
|
|
383
|
+
and(
|
|
384
|
+
eq(schema.recordings.id, recordingId),
|
|
385
|
+
ownerEmailMatches(schema.recordings.ownerEmail, ownerEmail),
|
|
386
|
+
),
|
|
387
|
+
);
|
|
388
|
+
if (
|
|
389
|
+
current?.status !== "uploading" ||
|
|
390
|
+
(current.uploadAttemptId ?? null) !==
|
|
391
|
+
(recoveryEnabled ? existingAttemptId : null) ||
|
|
392
|
+
(current.uploadGenerationId ?? null) !== nextGenerationId
|
|
393
|
+
) {
|
|
394
|
+
setResponseStatus(event, 409);
|
|
395
|
+
return {
|
|
396
|
+
error: "Recording upload changed while its retry was starting.",
|
|
397
|
+
staleAttempt: true,
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
}
|
|
235
401
|
|
|
236
402
|
// Stash compression metadata under its own key. We don't merge it into
|
|
237
403
|
// `recording-upload-{id}` because the recorder client overwrites that
|
|
@@ -245,25 +411,13 @@ export default defineEventHandler(async (event: H3Event) => {
|
|
|
245
411
|
});
|
|
246
412
|
}
|
|
247
413
|
|
|
248
|
-
await db
|
|
249
|
-
.update(schema.recordings)
|
|
250
|
-
.set({
|
|
251
|
-
status: "uploading",
|
|
252
|
-
failureReason: null,
|
|
253
|
-
uploadProgress: 0,
|
|
254
|
-
// A retry re-takes the lease. Without this the row would go back to
|
|
255
|
-
// 'uploading' still carrying the expired lease that killed it.
|
|
256
|
-
uploadLeaseExpiresAt: uploadLeaseExpiry(),
|
|
257
|
-
updatedAt: now,
|
|
258
|
-
})
|
|
259
|
-
.where(eq(schema.recordings.id, recordingId));
|
|
260
|
-
|
|
261
414
|
return {
|
|
262
415
|
ok: true,
|
|
263
416
|
recordingId,
|
|
264
417
|
chunksCleared: cleared,
|
|
265
418
|
compressionRecorded: !!compression,
|
|
266
419
|
uploadMode,
|
|
420
|
+
uploadGenerationId: nextGenerationId,
|
|
267
421
|
};
|
|
268
422
|
});
|
|
269
423
|
});
|
|
@@ -11,17 +11,25 @@
|
|
|
11
11
|
* Route: GET /api/uploads/:recordingId/resume
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
+
import {
|
|
15
|
+
compareAndSetAppState,
|
|
16
|
+
readAppState,
|
|
17
|
+
writeAppState,
|
|
18
|
+
} from "@agent-native/core/application-state";
|
|
19
|
+
import { isFeatureFlagEnabled } from "@agent-native/core/feature-flags";
|
|
14
20
|
import { runWithRequestContext } from "@agent-native/core/server";
|
|
15
|
-
import { and, eq } from "drizzle-orm";
|
|
21
|
+
import { and, eq, isNull } from "drizzle-orm";
|
|
16
22
|
import {
|
|
17
23
|
createError,
|
|
18
24
|
defineEventHandler,
|
|
19
25
|
getRouterParam,
|
|
26
|
+
getQuery,
|
|
20
27
|
setResponseHeader,
|
|
21
28
|
setResponseStatus,
|
|
22
29
|
type H3Event,
|
|
23
30
|
} from "h3";
|
|
24
31
|
|
|
32
|
+
import { UPLOAD_RETRY_RESUME_FLAG } from "../../../../../shared/feature-flags.js";
|
|
25
33
|
import { getDb, schema } from "../../../../db/index.js";
|
|
26
34
|
import {
|
|
27
35
|
listRecordingChunkKeys,
|
|
@@ -33,7 +41,11 @@ import {
|
|
|
33
41
|
ownerEmailMatches,
|
|
34
42
|
} from "../../../../lib/recordings.js";
|
|
35
43
|
import { getResumableSession } from "../../../../lib/resumable-session.js";
|
|
36
|
-
import {
|
|
44
|
+
import {
|
|
45
|
+
isRetryableUploadInterruption,
|
|
46
|
+
RETRYABLE_UPLOAD_INTERRUPTION_REASON,
|
|
47
|
+
} from "../../../../lib/upload-interruption.js";
|
|
48
|
+
import { uploadLeaseExpiry } from "../../../../lib/upload-lease.js";
|
|
37
49
|
|
|
38
50
|
export default defineEventHandler(async (event: H3Event) => {
|
|
39
51
|
setResponseHeader(event, "Cache-Control", "private, max-age=0, no-store");
|
|
@@ -41,6 +53,21 @@ export default defineEventHandler(async (event: H3Event) => {
|
|
|
41
53
|
if (!recordingId) {
|
|
42
54
|
throw createError({ statusCode: 400, message: "Missing recordingId" });
|
|
43
55
|
}
|
|
56
|
+
const requestedAttemptIdValue = getQuery(event).attemptId;
|
|
57
|
+
const requestedAttemptId = Array.isArray(requestedAttemptIdValue)
|
|
58
|
+
? requestedAttemptIdValue[0]
|
|
59
|
+
: requestedAttemptIdValue;
|
|
60
|
+
if (
|
|
61
|
+
typeof requestedAttemptId !== "string" ||
|
|
62
|
+
requestedAttemptId.length < 16 ||
|
|
63
|
+
requestedAttemptId.length > 128 ||
|
|
64
|
+
!/^[A-Za-z0-9_-]+$/.test(requestedAttemptId)
|
|
65
|
+
) {
|
|
66
|
+
throw createError({
|
|
67
|
+
statusCode: 400,
|
|
68
|
+
message: "A valid upload retry attemptId is required",
|
|
69
|
+
});
|
|
70
|
+
}
|
|
44
71
|
|
|
45
72
|
let ownerEmail: string;
|
|
46
73
|
let orgId: string | undefined;
|
|
@@ -52,6 +79,21 @@ export default defineEventHandler(async (event: H3Event) => {
|
|
|
52
79
|
throw createError({ statusCode: 401, message: "Unauthorized" });
|
|
53
80
|
}
|
|
54
81
|
|
|
82
|
+
const recoveryEnabled = await isFeatureFlagEnabled(UPLOAD_RETRY_RESUME_FLAG, {
|
|
83
|
+
userEmail: ownerEmail,
|
|
84
|
+
userKey: ownerEmail,
|
|
85
|
+
orgId,
|
|
86
|
+
});
|
|
87
|
+
if (!recoveryEnabled) {
|
|
88
|
+
return {
|
|
89
|
+
recoveryEnabled: false,
|
|
90
|
+
resumable: false,
|
|
91
|
+
recordingId,
|
|
92
|
+
status: null,
|
|
93
|
+
reason: "feature_disabled",
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
55
97
|
return runWithRequestContext({ userEmail: ownerEmail, orgId }, async () => {
|
|
56
98
|
const [recording] = await getDb()
|
|
57
99
|
.select({
|
|
@@ -59,6 +101,9 @@ export default defineEventHandler(async (event: H3Event) => {
|
|
|
59
101
|
status: schema.recordings.status,
|
|
60
102
|
failureReason: schema.recordings.failureReason,
|
|
61
103
|
videoUrl: schema.recordings.videoUrl,
|
|
104
|
+
uploadProgress: schema.recordings.uploadProgress,
|
|
105
|
+
uploadAttemptId: schema.recordings.uploadAttemptId,
|
|
106
|
+
uploadGenerationId: schema.recordings.uploadGenerationId,
|
|
62
107
|
})
|
|
63
108
|
.from(schema.recordings)
|
|
64
109
|
.where(
|
|
@@ -73,31 +118,160 @@ export default defineEventHandler(async (event: H3Event) => {
|
|
|
73
118
|
return { error: "Recording not found" };
|
|
74
119
|
}
|
|
75
120
|
|
|
76
|
-
|
|
77
|
-
|
|
121
|
+
// Legacy rows keep their null generation and unscoped scratch. A reset
|
|
122
|
+
// upgrades them by installing a fresh generation before it deletes data.
|
|
123
|
+
const existingGenerationId = recording.uploadGenerationId ?? null;
|
|
124
|
+
const generationId = existingGenerationId;
|
|
125
|
+
const session = generationId
|
|
126
|
+
? await getResumableSession(recordingId, generationId)
|
|
127
|
+
: await getResumableSession(recordingId);
|
|
128
|
+
const retryableFailure =
|
|
129
|
+
recording.status === "failed" &&
|
|
130
|
+
isRetryableUploadInterruption(recording.failureReason);
|
|
131
|
+
const existingAttemptId = recording.uploadAttemptId ?? null;
|
|
132
|
+
if (
|
|
133
|
+
recording.status === "uploading" &&
|
|
134
|
+
existingAttemptId !== null &&
|
|
135
|
+
existingAttemptId !== requestedAttemptId
|
|
136
|
+
) {
|
|
137
|
+
setResponseStatus(event, 409);
|
|
138
|
+
return {
|
|
139
|
+
resumable: false,
|
|
140
|
+
recoveryEnabled: true,
|
|
141
|
+
recordingId,
|
|
142
|
+
status: "uploading",
|
|
143
|
+
reason: "retry_already_active",
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
if (recording.status !== "uploading" && !retryableFailure) {
|
|
147
|
+
return {
|
|
148
|
+
resumable: false,
|
|
149
|
+
recoveryEnabled: true,
|
|
150
|
+
recordingId,
|
|
151
|
+
status: recording.status,
|
|
152
|
+
failureReason: recording.failureReason,
|
|
153
|
+
videoUrl: recording.videoUrl,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const uploadStateKey = `recording-upload-${recordingId}`;
|
|
158
|
+
const uploadStateRaw = await readAppState(uploadStateKey);
|
|
159
|
+
const uploadState = uploadStateRaw ?? {};
|
|
160
|
+
const attemptId = requestedAttemptId;
|
|
161
|
+
const now = new Date().toISOString();
|
|
162
|
+
const claimed = await getDb()
|
|
163
|
+
.update(schema.recordings)
|
|
164
|
+
.set({
|
|
165
|
+
status: "uploading",
|
|
166
|
+
failureReason: null,
|
|
167
|
+
uploadAttemptId: attemptId,
|
|
168
|
+
...(generationId ? { uploadGenerationId: generationId } : {}),
|
|
169
|
+
uploadLeaseExpiresAt: uploadLeaseExpiry(),
|
|
170
|
+
updatedAt: now,
|
|
171
|
+
})
|
|
172
|
+
.where(
|
|
173
|
+
and(
|
|
174
|
+
eq(schema.recordings.id, recordingId),
|
|
175
|
+
ownerEmailMatches(schema.recordings.ownerEmail, ownerEmail),
|
|
176
|
+
retryableFailure
|
|
177
|
+
? eq(schema.recordings.status, "failed")
|
|
178
|
+
: eq(schema.recordings.status, "uploading"),
|
|
179
|
+
retryableFailure
|
|
180
|
+
? eq(
|
|
181
|
+
schema.recordings.failureReason,
|
|
182
|
+
RETRYABLE_UPLOAD_INTERRUPTION_REASON,
|
|
183
|
+
)
|
|
184
|
+
: undefined,
|
|
185
|
+
existingAttemptId === null
|
|
186
|
+
? isNull(schema.recordings.uploadAttemptId)
|
|
187
|
+
: eq(schema.recordings.uploadAttemptId, existingAttemptId),
|
|
188
|
+
existingGenerationId === null
|
|
189
|
+
? isNull(schema.recordings.uploadGenerationId)
|
|
190
|
+
: eq(schema.recordings.uploadGenerationId, existingGenerationId),
|
|
191
|
+
),
|
|
192
|
+
)
|
|
193
|
+
.returning({ id: schema.recordings.id });
|
|
194
|
+
|
|
195
|
+
if (claimed.length !== 1) {
|
|
196
|
+
setResponseStatus(event, 409);
|
|
78
197
|
return {
|
|
79
198
|
resumable: false,
|
|
199
|
+
recoveryEnabled: true,
|
|
80
200
|
recordingId,
|
|
81
|
-
status:
|
|
82
|
-
|
|
83
|
-
videoUrl: lease.videoUrl,
|
|
201
|
+
status: "uploading",
|
|
202
|
+
reason: "retry_already_active",
|
|
84
203
|
};
|
|
85
204
|
}
|
|
86
205
|
|
|
87
|
-
const
|
|
206
|
+
const uploadStateUpdated = await compareAndSetAppState(
|
|
207
|
+
uploadStateKey,
|
|
208
|
+
uploadStateRaw,
|
|
209
|
+
{
|
|
210
|
+
...uploadState,
|
|
211
|
+
recordingId,
|
|
212
|
+
status: "uploading",
|
|
213
|
+
failureReason: null,
|
|
214
|
+
retryableInterruption: false,
|
|
215
|
+
progress: recording.uploadProgress,
|
|
216
|
+
uploadAttemptId: attemptId,
|
|
217
|
+
uploadGenerationId: generationId,
|
|
218
|
+
...(session ? { bytesReceived: session.bytesUploaded } : {}),
|
|
219
|
+
updatedAt: now,
|
|
220
|
+
},
|
|
221
|
+
);
|
|
222
|
+
if (!uploadStateUpdated) {
|
|
223
|
+
const [current] = await getDb()
|
|
224
|
+
.select({
|
|
225
|
+
status: schema.recordings.status,
|
|
226
|
+
uploadAttemptId: schema.recordings.uploadAttemptId,
|
|
227
|
+
uploadGenerationId: schema.recordings.uploadGenerationId,
|
|
228
|
+
})
|
|
229
|
+
.from(schema.recordings)
|
|
230
|
+
.where(
|
|
231
|
+
and(
|
|
232
|
+
eq(schema.recordings.id, recordingId),
|
|
233
|
+
ownerEmailMatches(schema.recordings.ownerEmail, ownerEmail),
|
|
234
|
+
),
|
|
235
|
+
);
|
|
236
|
+
if (
|
|
237
|
+
current?.status !== "uploading" ||
|
|
238
|
+
(current.uploadAttemptId ?? null) !== attemptId ||
|
|
239
|
+
(current.uploadGenerationId ?? null) !== generationId
|
|
240
|
+
) {
|
|
241
|
+
setResponseStatus(event, 409);
|
|
242
|
+
return {
|
|
243
|
+
resumable: false,
|
|
244
|
+
recoveryEnabled: true,
|
|
245
|
+
recordingId,
|
|
246
|
+
status: current?.status ?? null,
|
|
247
|
+
reason: "upload_state_changed",
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
console.info(
|
|
251
|
+
`[upload-resume] upload state advanced under the same claim; preserving newer progress for ${recordingId}`,
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
await writeAppState("refresh-signal", { ts: Date.now() });
|
|
255
|
+
|
|
88
256
|
if (session) {
|
|
89
257
|
return {
|
|
90
258
|
resumable: true,
|
|
259
|
+
recoveryEnabled: true,
|
|
91
260
|
recordingId,
|
|
92
|
-
status:
|
|
261
|
+
status: "uploading",
|
|
93
262
|
uploadMode: "streaming" as const,
|
|
263
|
+
attemptId,
|
|
264
|
+
...(generationId ? { uploadGenerationId: generationId } : {}),
|
|
94
265
|
bytesReceived: session.bytesUploaded,
|
|
95
266
|
nextChunkIndex: (session.lastCommittedIndex ?? -1) + 1,
|
|
96
267
|
};
|
|
97
268
|
}
|
|
98
269
|
|
|
99
270
|
const stored = new Set(
|
|
100
|
-
(
|
|
271
|
+
(generationId
|
|
272
|
+
? await listRecordingChunkKeys(ownerEmail, recordingId, generationId)
|
|
273
|
+
: await listRecordingChunkKeys(ownerEmail, recordingId)
|
|
274
|
+
)
|
|
101
275
|
.map(recordingChunkIndexFromKey)
|
|
102
276
|
.filter((index): index is number => index !== null),
|
|
103
277
|
);
|
|
@@ -108,10 +282,15 @@ export default defineEventHandler(async (event: H3Event) => {
|
|
|
108
282
|
|
|
109
283
|
return {
|
|
110
284
|
resumable: true,
|
|
285
|
+
recoveryEnabled: true,
|
|
111
286
|
recordingId,
|
|
112
|
-
status:
|
|
287
|
+
status: "uploading",
|
|
113
288
|
uploadMode: "buffered" as const,
|
|
114
|
-
|
|
289
|
+
attemptId,
|
|
290
|
+
...(generationId ? { uploadGenerationId: generationId } : {}),
|
|
291
|
+
bytesReceived: generationId
|
|
292
|
+
? await sumRecordingChunkBytes(ownerEmail, recordingId, generationId)
|
|
293
|
+
: await sumRecordingChunkBytes(ownerEmail, recordingId),
|
|
115
294
|
nextChunkIndex,
|
|
116
295
|
};
|
|
117
296
|
});
|
|
@@ -27,10 +27,16 @@ export type ClipsAiPrefs = {
|
|
|
27
27
|
|
|
28
28
|
export type ClipsUserPrefs = ClipsAiPrefs & {
|
|
29
29
|
defaultPlaybackSpeed?: string;
|
|
30
|
+
/** Activity emails (comments, replies, reactions) only — never share invites. */
|
|
30
31
|
emailNotifications?: boolean;
|
|
31
32
|
transcriptCleanupEnabled?: boolean;
|
|
33
|
+
/** Overrides the organization default visibility for new recordings. */
|
|
34
|
+
defaultRecordingVisibility?: ClipsDefaultVisibility;
|
|
32
35
|
};
|
|
33
36
|
|
|
37
|
+
/** Visibility applied to new recordings unless the creator picks another. */
|
|
38
|
+
export type ClipsDefaultVisibility = "private" | "org" | "public";
|
|
39
|
+
|
|
34
40
|
export function isIncludeFullVideoInAiEnabled(
|
|
35
41
|
prefs: ClipsAiPrefs | Record<string, unknown> | null | undefined,
|
|
36
42
|
): boolean {
|
|
@@ -28,7 +28,19 @@ export const CUSTOM_SCK_LIVE_UPLOAD_FLAG = defineFeatureFlag({
|
|
|
28
28
|
"Upload recording chunks while capture is still in progress. Requires the custom ScreenCaptureKit pipeline.",
|
|
29
29
|
});
|
|
30
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Desktop app and hosted upload routes: preserve interrupted resumable
|
|
33
|
+
* sessions and continue saved retries from the provider-confirmed offset.
|
|
34
|
+
*/
|
|
35
|
+
export const UPLOAD_RETRY_RESUME_FLAG = defineFeatureFlag({
|
|
36
|
+
key: "uploadRetryResume",
|
|
37
|
+
displayName: "Resumable upload retry",
|
|
38
|
+
description:
|
|
39
|
+
"Resume interrupted desktop uploads from their last confirmed byte instead of replaying the full local backup.",
|
|
40
|
+
});
|
|
41
|
+
|
|
31
42
|
export const CLIPS_FEATURE_FLAGS = defineFeatureFlags([
|
|
32
43
|
USE_CUSTOM_SCK_PIPELINE_FLAG,
|
|
33
44
|
CUSTOM_SCK_LIVE_UPLOAD_FLAG,
|
|
45
|
+
UPLOAD_RETRY_RESUME_FLAG,
|
|
34
46
|
]);
|
|
@@ -76,6 +76,7 @@ export type ChunkUploadParams = {
|
|
|
76
76
|
height?: number | null;
|
|
77
77
|
hasAudio?: boolean;
|
|
78
78
|
hasCamera?: boolean;
|
|
79
|
+
uploadGenerationId?: string;
|
|
79
80
|
};
|
|
80
81
|
|
|
81
82
|
export function normalizeChunkUploadNumber(value: unknown): number | undefined {
|
|
@@ -115,6 +116,9 @@ export function chunkUploadQuery(params: ChunkUploadParams): string {
|
|
|
115
116
|
if (params.hasCamera !== undefined) {
|
|
116
117
|
q.set("hasCamera", params.hasCamera ? "1" : "0");
|
|
117
118
|
}
|
|
119
|
+
if (params.uploadGenerationId) {
|
|
120
|
+
q.set("uploadGenerationId", params.uploadGenerationId);
|
|
121
|
+
}
|
|
118
122
|
return q.toString();
|
|
119
123
|
}
|
|
120
124
|
|