@agent-native/core 0.134.1 → 0.135.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/corpus/README.md +3 -3
- package/corpus/core/CHANGELOG.md +107 -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/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/add-comment.ts +15 -1
- package/corpus/templates/content/actions/list-documents.ts +1 -0
- package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +5 -2
- package/corpus/templates/content/app/components/editor/DocumentToolbar.tsx +50 -1
- package/corpus/templates/content/app/components/ui/switch.tsx +1 -0
- package/corpus/templates/content/app/hooks/use-content-prefs.ts +55 -0
- package/corpus/templates/content/app/i18n-data.ts +40 -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/server/lib/comment-notifications.ts +146 -0
- 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/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/collab/struct-routes.d.ts +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 +1 -1
- package/dist/observability/routes.d.ts +1 -1
- 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/provider-api/actions/custom-provider-registration.d.ts +10 -10
- package/dist/provider-api/actions/provider-api.d.ts +8 -8
- package/dist/provider-api/corpus-jobs.d.ts +2 -2
- package/dist/resources/handlers.d.ts +1 -1
- 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/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/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
|
@@ -88,6 +88,13 @@ import {
|
|
|
88
88
|
type CapturedTranscript,
|
|
89
89
|
type TranscriptionCapture,
|
|
90
90
|
} from "./transcription-capture";
|
|
91
|
+
import {
|
|
92
|
+
buildStreamingReplayPlan,
|
|
93
|
+
planStreamingRecovery,
|
|
94
|
+
retryAttemptIdAfterRestartSignal,
|
|
95
|
+
retryAttemptIdAfterResumeResponse,
|
|
96
|
+
type UploadResumeResponse,
|
|
97
|
+
} from "./upload-recovery";
|
|
91
98
|
import {
|
|
92
99
|
parseFinalizeReceipt,
|
|
93
100
|
verifyFinalizeReceipt,
|
|
@@ -310,6 +317,7 @@ export interface PendingBrowserRecordingUpload {
|
|
|
310
317
|
retryCount: number;
|
|
311
318
|
chunkCount: number;
|
|
312
319
|
mimeType: string;
|
|
320
|
+
uploadAttemptId?: string | null;
|
|
313
321
|
}
|
|
314
322
|
|
|
315
323
|
function streamFromTracks(tracks: MediaStreamTrack[]): MediaStream {
|
|
@@ -983,6 +991,16 @@ function buildRetryHeaders(mimeType: string, authToken?: string): Headers {
|
|
|
983
991
|
return headers;
|
|
984
992
|
}
|
|
985
993
|
|
|
994
|
+
class UploadRestartRequiredError extends Error {
|
|
995
|
+
constructor(
|
|
996
|
+
message: string,
|
|
997
|
+
readonly recoveryEnabled?: boolean,
|
|
998
|
+
) {
|
|
999
|
+
super(message);
|
|
1000
|
+
this.name = "UploadRestartRequiredError";
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
|
|
986
1004
|
async function postBackupChunk(
|
|
987
1005
|
url: string,
|
|
988
1006
|
blob: Blob,
|
|
@@ -999,6 +1017,24 @@ async function postBackupChunk(
|
|
|
999
1017
|
});
|
|
1000
1018
|
const body = await res.text().catch(() => "");
|
|
1001
1019
|
if (!res.ok) {
|
|
1020
|
+
const details = (() => {
|
|
1021
|
+
try {
|
|
1022
|
+
return JSON.parse(body) as {
|
|
1023
|
+
restartRequired?: unknown;
|
|
1024
|
+
recoveryEnabled?: unknown;
|
|
1025
|
+
};
|
|
1026
|
+
} catch {
|
|
1027
|
+
return null;
|
|
1028
|
+
}
|
|
1029
|
+
})();
|
|
1030
|
+
if (details?.restartRequired === true) {
|
|
1031
|
+
throw new UploadRestartRequiredError(
|
|
1032
|
+
`The prior upload session expired (${res.status})`,
|
|
1033
|
+
typeof details.recoveryEnabled === "boolean"
|
|
1034
|
+
? details.recoveryEnabled
|
|
1035
|
+
: undefined,
|
|
1036
|
+
);
|
|
1037
|
+
}
|
|
1002
1038
|
throw new Error(
|
|
1003
1039
|
`Upload retry failed (${res.status}): ${body.slice(0, 200)}`,
|
|
1004
1040
|
);
|
|
@@ -1009,7 +1045,9 @@ async function postBackupChunk(
|
|
|
1009
1045
|
async function resetBrowserRecordingBackupUpload(
|
|
1010
1046
|
meta: BrowserRecordingBackupMeta,
|
|
1011
1047
|
authToken?: string,
|
|
1012
|
-
|
|
1048
|
+
attemptId?: string,
|
|
1049
|
+
uploadGenerationId?: string,
|
|
1050
|
+
): Promise<{ uploadMode: UploadMode; uploadGenerationId?: string }> {
|
|
1013
1051
|
const res = await fetch(
|
|
1014
1052
|
`${meta.serverUrl.replace(/\/+$/, "")}/api/uploads/${meta.recordingId}/reset-chunks`,
|
|
1015
1053
|
{
|
|
@@ -1023,6 +1061,8 @@ async function resetBrowserRecordingBackupUpload(
|
|
|
1023
1061
|
body: JSON.stringify({
|
|
1024
1062
|
requestStreaming: true,
|
|
1025
1063
|
mimeType: meta.mimeType,
|
|
1064
|
+
...(attemptId ? { attemptId } : {}),
|
|
1065
|
+
...(uploadGenerationId ? { uploadGenerationId } : {}),
|
|
1026
1066
|
}),
|
|
1027
1067
|
},
|
|
1028
1068
|
);
|
|
@@ -1034,14 +1074,61 @@ async function resetBrowserRecordingBackupUpload(
|
|
|
1034
1074
|
}
|
|
1035
1075
|
const body = (await res.json().catch(() => null)) as {
|
|
1036
1076
|
uploadMode?: unknown;
|
|
1077
|
+
uploadGenerationId?: unknown;
|
|
1037
1078
|
} | null;
|
|
1038
|
-
|
|
1079
|
+
if (attemptId && typeof body?.uploadGenerationId !== "string") {
|
|
1080
|
+
throw new Error("Upload retry setup returned no upload generation");
|
|
1081
|
+
}
|
|
1082
|
+
return {
|
|
1083
|
+
uploadMode: body?.uploadMode === "streaming" ? "streaming" : "buffered",
|
|
1084
|
+
...(typeof body?.uploadGenerationId === "string"
|
|
1085
|
+
? { uploadGenerationId: body.uploadGenerationId }
|
|
1086
|
+
: {}),
|
|
1087
|
+
};
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1090
|
+
async function getBrowserRecordingUploadResume(
|
|
1091
|
+
meta: BrowserRecordingBackupMeta,
|
|
1092
|
+
attemptId: string,
|
|
1093
|
+
authToken?: string,
|
|
1094
|
+
): Promise<UploadResumeResponse> {
|
|
1095
|
+
const resumeUrl = new URL(
|
|
1096
|
+
`${meta.serverUrl.replace(/\/+$/, "")}/api/uploads/${meta.recordingId}/resume`,
|
|
1097
|
+
);
|
|
1098
|
+
resumeUrl.searchParams.set("attemptId", attemptId);
|
|
1099
|
+
const res = await fetch(resumeUrl, {
|
|
1100
|
+
method: "GET",
|
|
1101
|
+
headers: buildRetryHeaders("application/json", authToken),
|
|
1102
|
+
credentials: "include",
|
|
1103
|
+
});
|
|
1104
|
+
const body = await res.text().catch(() => "");
|
|
1105
|
+
if (!res.ok) {
|
|
1106
|
+
throw new Error(
|
|
1107
|
+
`Upload resume check failed (${res.status}): ${body.slice(0, 200)}`,
|
|
1108
|
+
);
|
|
1109
|
+
}
|
|
1110
|
+
let parsed: UploadResumeResponse;
|
|
1111
|
+
try {
|
|
1112
|
+
parsed = JSON.parse(body) as UploadResumeResponse;
|
|
1113
|
+
} catch {
|
|
1114
|
+
throw new Error("Upload resume check returned an unreadable response");
|
|
1115
|
+
}
|
|
1116
|
+
if (parsed.resumable && parsed.attemptId !== attemptId) {
|
|
1117
|
+
throw new Error("Upload resume check returned a mismatched retry token");
|
|
1118
|
+
}
|
|
1119
|
+
return parsed;
|
|
1039
1120
|
}
|
|
1040
1121
|
|
|
1041
1122
|
async function replayBrowserBackupToResumableSession(
|
|
1042
1123
|
meta: BrowserRecordingBackupMeta,
|
|
1043
1124
|
chunks: BrowserRecordingBackupChunk[],
|
|
1044
1125
|
authToken?: string,
|
|
1126
|
+
attemptId?: string,
|
|
1127
|
+
uploadGenerationId?: string,
|
|
1128
|
+
resumeFrom: { bytesReceived: number; nextChunkIndex: number } = {
|
|
1129
|
+
bytesReceived: 0,
|
|
1130
|
+
nextChunkIndex: 0,
|
|
1131
|
+
},
|
|
1045
1132
|
): Promise<FinalizeReceipt | null> {
|
|
1046
1133
|
// The backup is stored in raw MediaRecorder blobs, which have arbitrary
|
|
1047
1134
|
// boundaries. A resumable provider needs every non-final request aligned,
|
|
@@ -1056,33 +1143,38 @@ async function replayBrowserBackupToResumableSession(
|
|
|
1056
1143
|
throw new Error("Local recording backup is empty");
|
|
1057
1144
|
}
|
|
1058
1145
|
|
|
1059
|
-
const
|
|
1060
|
-
|
|
1061
|
-
|
|
1146
|
+
const replayPlan = buildStreamingReplayPlan({
|
|
1147
|
+
localBytes: recording.size,
|
|
1148
|
+
chunkBytes: STREAM_CHUNK_BYTES,
|
|
1149
|
+
...resumeFrom,
|
|
1150
|
+
});
|
|
1151
|
+
const totalPosts = Math.floor(recording.size / STREAM_CHUNK_BYTES) + 1;
|
|
1062
1152
|
|
|
1063
|
-
for (
|
|
1064
|
-
const body = recording.slice(
|
|
1065
|
-
offset,
|
|
1066
|
-
offset + STREAM_CHUNK_BYTES,
|
|
1067
|
-
meta.mimeType,
|
|
1068
|
-
);
|
|
1153
|
+
for (const request of replayPlan.filter((item) => !item.final)) {
|
|
1154
|
+
const body = recording.slice(request.start, request.end, meta.mimeType);
|
|
1069
1155
|
await postBackupChunk(
|
|
1070
|
-
chunkUrl(meta.serverUrl, meta.recordingId, index, false, {
|
|
1156
|
+
chunkUrl(meta.serverUrl, meta.recordingId, request.index, false, {
|
|
1071
1157
|
total: String(totalPosts),
|
|
1072
1158
|
mimeType: meta.mimeType,
|
|
1159
|
+
...(attemptId ? { attemptId } : {}),
|
|
1160
|
+
...(uploadGenerationId ? { uploadGenerationId } : {}),
|
|
1073
1161
|
}),
|
|
1074
1162
|
body,
|
|
1075
1163
|
authToken,
|
|
1076
1164
|
);
|
|
1077
|
-
offset += STREAM_CHUNK_BYTES;
|
|
1078
1165
|
}
|
|
1079
1166
|
|
|
1080
1167
|
// The final post always closes the session. When the file is exactly
|
|
1081
1168
|
// aligned it is intentionally empty; the route sends the provider's close
|
|
1082
1169
|
// request with the bytes committed by the previous chunks.
|
|
1083
|
-
const
|
|
1170
|
+
const finalRequest = replayPlan[replayPlan.length - 1]!;
|
|
1171
|
+
const finalBody = recording.slice(
|
|
1172
|
+
finalRequest.start,
|
|
1173
|
+
finalRequest.end,
|
|
1174
|
+
meta.mimeType,
|
|
1175
|
+
);
|
|
1084
1176
|
return postBackupChunk(
|
|
1085
|
-
chunkUrl(meta.serverUrl, meta.recordingId,
|
|
1177
|
+
chunkUrl(meta.serverUrl, meta.recordingId, finalRequest.index, true, {
|
|
1086
1178
|
total: String(totalPosts),
|
|
1087
1179
|
mimeType: meta.mimeType,
|
|
1088
1180
|
durationMs: String(Math.round(meta.durationMs || 0)),
|
|
@@ -1090,6 +1182,8 @@ async function replayBrowserBackupToResumableSession(
|
|
|
1090
1182
|
...(meta.height ? { height: String(meta.height) } : {}),
|
|
1091
1183
|
hasAudio: meta.hasAudio ? "1" : "0",
|
|
1092
1184
|
hasCamera: meta.hasCamera ? "1" : "0",
|
|
1185
|
+
...(attemptId ? { attemptId } : {}),
|
|
1186
|
+
...(uploadGenerationId ? { uploadGenerationId } : {}),
|
|
1093
1187
|
}),
|
|
1094
1188
|
finalBody,
|
|
1095
1189
|
authToken,
|
|
@@ -1100,6 +1194,10 @@ export async function retryBrowserRecordingBackup(input: {
|
|
|
1100
1194
|
recordingId: string;
|
|
1101
1195
|
serverUrl?: string;
|
|
1102
1196
|
authToken?: string;
|
|
1197
|
+
onRecoveryDecision?: (decision: {
|
|
1198
|
+
action: "resume" | "restart" | "reconcile";
|
|
1199
|
+
progress: number;
|
|
1200
|
+
}) => void;
|
|
1103
1201
|
}): Promise<{ recordingId: string; viewUrl: string }> {
|
|
1104
1202
|
let meta = await getBrowserRecordingBackupMeta(input.recordingId);
|
|
1105
1203
|
if (!meta) {
|
|
@@ -1111,57 +1209,165 @@ export async function retryBrowserRecordingBackup(input: {
|
|
|
1111
1209
|
}
|
|
1112
1210
|
const chunks = await getBrowserRecordingBackupChunks(input.recordingId);
|
|
1113
1211
|
const validatedChunks = validateBrowserRecordingBackupChunks(meta, chunks);
|
|
1212
|
+
let activeAttemptId: string | undefined =
|
|
1213
|
+
meta.uploadAttemptId || crypto.randomUUID();
|
|
1214
|
+
let activeUploadGenerationId: string | undefined;
|
|
1114
1215
|
|
|
1115
1216
|
try {
|
|
1116
1217
|
await putBrowserRecordingBackupMeta({
|
|
1117
1218
|
...meta,
|
|
1219
|
+
uploadAttemptId: activeAttemptId,
|
|
1118
1220
|
lastAttemptAt: new Date().toISOString(),
|
|
1119
1221
|
lastError: null,
|
|
1120
1222
|
});
|
|
1121
|
-
const
|
|
1223
|
+
const recordingBytes = validatedChunks.reduce(
|
|
1224
|
+
(total, chunk) => total + chunk.blob.size,
|
|
1225
|
+
0,
|
|
1226
|
+
);
|
|
1227
|
+
const resumeResponse = await getBrowserRecordingUploadResume(
|
|
1122
1228
|
meta,
|
|
1229
|
+
activeAttemptId,
|
|
1123
1230
|
input.authToken,
|
|
1124
1231
|
);
|
|
1232
|
+
const recoveryPlan = planStreamingRecovery({
|
|
1233
|
+
response: resumeResponse,
|
|
1234
|
+
localBytes: recordingBytes,
|
|
1235
|
+
chunkBytes: STREAM_CHUNK_BYTES,
|
|
1236
|
+
});
|
|
1237
|
+
activeAttemptId = retryAttemptIdAfterResumeResponse(
|
|
1238
|
+
activeAttemptId,
|
|
1239
|
+
resumeResponse,
|
|
1240
|
+
);
|
|
1241
|
+
activeUploadGenerationId = resumeResponse.resumable
|
|
1242
|
+
? resumeResponse.uploadGenerationId
|
|
1243
|
+
: undefined;
|
|
1244
|
+
if (recoveryPlan.action === "reconcile") {
|
|
1245
|
+
input.onRecoveryDecision?.({ action: "reconcile", progress: 1 });
|
|
1246
|
+
if (
|
|
1247
|
+
await recoverAcceptedRecordingAfterFinalizeError({
|
|
1248
|
+
serverUrl: meta.serverUrl,
|
|
1249
|
+
recordingId: meta.recordingId,
|
|
1250
|
+
authToken: input.authToken,
|
|
1251
|
+
})
|
|
1252
|
+
) {
|
|
1253
|
+
return {
|
|
1254
|
+
recordingId: meta.recordingId,
|
|
1255
|
+
viewUrl: `/r/${meta.recordingId}`,
|
|
1256
|
+
};
|
|
1257
|
+
}
|
|
1258
|
+
throw new Error(
|
|
1259
|
+
`Upload is ${recoveryPlan.status}, but its accepted media could not be verified`,
|
|
1260
|
+
);
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
let uploadMode: UploadMode = "streaming";
|
|
1264
|
+
let resumeFrom = { bytesReceived: 0, nextChunkIndex: 0 };
|
|
1265
|
+
if (recoveryPlan.action === "resume") {
|
|
1266
|
+
resumeFrom = recoveryPlan;
|
|
1267
|
+
input.onRecoveryDecision?.({
|
|
1268
|
+
action: "resume",
|
|
1269
|
+
progress: recoveryPlan.progress,
|
|
1270
|
+
});
|
|
1271
|
+
console.info("[clips-recorder] resuming saved upload", {
|
|
1272
|
+
recordingId: meta.recordingId,
|
|
1273
|
+
localBytes: recordingBytes,
|
|
1274
|
+
serverAcknowledgedBytes: recoveryPlan.bytesReceived,
|
|
1275
|
+
nextChunkIndex: recoveryPlan.nextChunkIndex,
|
|
1276
|
+
progress: recoveryPlan.progress,
|
|
1277
|
+
});
|
|
1278
|
+
} else {
|
|
1279
|
+
input.onRecoveryDecision?.({ action: "restart", progress: 0 });
|
|
1280
|
+
console.info("[clips-recorder] restarting saved upload", {
|
|
1281
|
+
recordingId: meta.recordingId,
|
|
1282
|
+
localBytes: recordingBytes,
|
|
1283
|
+
reason: recoveryPlan.reason,
|
|
1284
|
+
});
|
|
1285
|
+
const reset = await resetBrowserRecordingBackupUpload(
|
|
1286
|
+
meta,
|
|
1287
|
+
input.authToken,
|
|
1288
|
+
activeAttemptId,
|
|
1289
|
+
activeUploadGenerationId,
|
|
1290
|
+
);
|
|
1291
|
+
uploadMode = reset.uploadMode;
|
|
1292
|
+
activeUploadGenerationId = reset.uploadGenerationId;
|
|
1293
|
+
}
|
|
1125
1294
|
|
|
1126
1295
|
if (uploadMode === "streaming") {
|
|
1296
|
+
let receipt: FinalizeReceipt | null = null;
|
|
1127
1297
|
try {
|
|
1128
|
-
|
|
1298
|
+
receipt = await replayBrowserBackupToResumableSession(
|
|
1129
1299
|
meta,
|
|
1130
1300
|
validatedChunks,
|
|
1131
1301
|
input.authToken,
|
|
1302
|
+
activeAttemptId,
|
|
1303
|
+
activeUploadGenerationId,
|
|
1304
|
+
resumeFrom,
|
|
1132
1305
|
);
|
|
1133
|
-
|
|
1134
|
-
if (
|
|
1135
|
-
|
|
1306
|
+
} catch (err) {
|
|
1307
|
+
if (err instanceof UploadRestartRequiredError) {
|
|
1308
|
+
activeAttemptId = retryAttemptIdAfterRestartSignal(
|
|
1309
|
+
activeAttemptId,
|
|
1310
|
+
err.recoveryEnabled,
|
|
1311
|
+
);
|
|
1312
|
+
if (err.recoveryEnabled === false) {
|
|
1313
|
+
activeUploadGenerationId = undefined;
|
|
1314
|
+
}
|
|
1315
|
+
input.onRecoveryDecision?.({ action: "restart", progress: 0 });
|
|
1316
|
+
console.info("[clips-recorder] restarting expired upload session", {
|
|
1317
|
+
recordingId: meta.recordingId,
|
|
1318
|
+
localBytes: recordingBytes,
|
|
1319
|
+
});
|
|
1320
|
+
const reset = await resetBrowserRecordingBackupUpload(
|
|
1321
|
+
meta,
|
|
1322
|
+
input.authToken,
|
|
1323
|
+
activeAttemptId,
|
|
1324
|
+
activeUploadGenerationId,
|
|
1325
|
+
);
|
|
1326
|
+
uploadMode = reset.uploadMode;
|
|
1327
|
+
activeUploadGenerationId = reset.uploadGenerationId;
|
|
1328
|
+
if (uploadMode === "streaming") {
|
|
1329
|
+
receipt = await replayBrowserBackupToResumableSession(
|
|
1330
|
+
meta,
|
|
1331
|
+
validatedChunks,
|
|
1332
|
+
input.authToken,
|
|
1333
|
+
activeAttemptId,
|
|
1334
|
+
activeUploadGenerationId,
|
|
1335
|
+
);
|
|
1336
|
+
}
|
|
1337
|
+
} else if (
|
|
1338
|
+
await recoverAcceptedRecordingAfterFinalizeError({
|
|
1136
1339
|
serverUrl: meta.serverUrl,
|
|
1137
1340
|
recordingId: meta.recordingId,
|
|
1138
1341
|
authToken: input.authToken,
|
|
1139
|
-
})
|
|
1342
|
+
})
|
|
1343
|
+
) {
|
|
1140
1344
|
return {
|
|
1141
1345
|
recordingId: meta.recordingId,
|
|
1142
1346
|
viewUrl: `/r/${meta.recordingId}`,
|
|
1143
1347
|
};
|
|
1348
|
+
} else {
|
|
1349
|
+
throw err;
|
|
1144
1350
|
}
|
|
1145
|
-
}
|
|
1146
|
-
|
|
1147
|
-
|
|
1351
|
+
}
|
|
1352
|
+
if (uploadMode === "streaming") {
|
|
1353
|
+
const receiptStatus = verifyFinalizeReceipt(receipt, meta);
|
|
1354
|
+
if (receiptStatus === "processing") {
|
|
1355
|
+
scheduleBrowserBackupCleanupAfterProcessing({
|
|
1148
1356
|
serverUrl: meta.serverUrl,
|
|
1149
1357
|
recordingId: meta.recordingId,
|
|
1150
1358
|
authToken: input.authToken,
|
|
1151
|
-
})
|
|
1152
|
-
) {
|
|
1359
|
+
});
|
|
1153
1360
|
return {
|
|
1154
1361
|
recordingId: meta.recordingId,
|
|
1155
1362
|
viewUrl: `/r/${meta.recordingId}`,
|
|
1156
1363
|
};
|
|
1157
1364
|
}
|
|
1158
|
-
|
|
1365
|
+
await deleteBrowserRecordingBackup(meta.recordingId);
|
|
1366
|
+
return {
|
|
1367
|
+
recordingId: meta.recordingId,
|
|
1368
|
+
viewUrl: `/r/${meta.recordingId}`,
|
|
1369
|
+
};
|
|
1159
1370
|
}
|
|
1160
|
-
await deleteBrowserRecordingBackup(meta.recordingId);
|
|
1161
|
-
return {
|
|
1162
|
-
recordingId: meta.recordingId,
|
|
1163
|
-
viewUrl: `/r/${meta.recordingId}`,
|
|
1164
|
-
};
|
|
1165
1371
|
}
|
|
1166
1372
|
|
|
1167
1373
|
const totalPosts = validatedChunks.length + 1;
|
|
@@ -1170,6 +1376,10 @@ export async function retryBrowserRecordingBackup(input: {
|
|
|
1170
1376
|
chunkUrl(meta.serverUrl, meta.recordingId, chunk.index, false, {
|
|
1171
1377
|
total: String(totalPosts),
|
|
1172
1378
|
mimeType: meta.mimeType,
|
|
1379
|
+
...(activeAttemptId ? { attemptId: activeAttemptId } : {}),
|
|
1380
|
+
...(activeUploadGenerationId
|
|
1381
|
+
? { uploadGenerationId: activeUploadGenerationId }
|
|
1382
|
+
: {}),
|
|
1173
1383
|
}),
|
|
1174
1384
|
chunk.blob,
|
|
1175
1385
|
input.authToken,
|
|
@@ -1189,6 +1399,10 @@ export async function retryBrowserRecordingBackup(input: {
|
|
|
1189
1399
|
...(meta.height ? { height: String(meta.height) } : {}),
|
|
1190
1400
|
hasAudio: meta.hasAudio ? "1" : "0",
|
|
1191
1401
|
hasCamera: meta.hasCamera ? "1" : "0",
|
|
1402
|
+
...(activeAttemptId ? { attemptId: activeAttemptId } : {}),
|
|
1403
|
+
...(activeUploadGenerationId
|
|
1404
|
+
? { uploadGenerationId: activeUploadGenerationId }
|
|
1405
|
+
: {}),
|
|
1192
1406
|
},
|
|
1193
1407
|
);
|
|
1194
1408
|
try {
|
|
@@ -1229,9 +1443,29 @@ export async function retryBrowserRecordingBackup(input: {
|
|
|
1229
1443
|
return { recordingId: meta.recordingId, viewUrl: `/r/${meta.recordingId}` };
|
|
1230
1444
|
} catch (err) {
|
|
1231
1445
|
const message = err instanceof Error ? err.message : String(err);
|
|
1446
|
+
if (
|
|
1447
|
+
await recoverAcceptedRecordingAfterFinalizeError({
|
|
1448
|
+
serverUrl: meta.serverUrl,
|
|
1449
|
+
recordingId: meta.recordingId,
|
|
1450
|
+
authToken: input.authToken,
|
|
1451
|
+
})
|
|
1452
|
+
) {
|
|
1453
|
+
return {
|
|
1454
|
+
recordingId: meta.recordingId,
|
|
1455
|
+
viewUrl: `/r/${meta.recordingId}`,
|
|
1456
|
+
};
|
|
1457
|
+
}
|
|
1232
1458
|
await markBrowserRecordingBackupError(meta.recordingId, message).catch(
|
|
1233
1459
|
() => {},
|
|
1234
1460
|
);
|
|
1461
|
+
await interruptRecordingUpload(
|
|
1462
|
+
meta.serverUrl,
|
|
1463
|
+
meta.recordingId,
|
|
1464
|
+
message,
|
|
1465
|
+
input.authToken,
|
|
1466
|
+
activeAttemptId,
|
|
1467
|
+
activeUploadGenerationId,
|
|
1468
|
+
);
|
|
1235
1469
|
throw err;
|
|
1236
1470
|
}
|
|
1237
1471
|
}
|
|
@@ -1610,6 +1844,55 @@ async function abortRecordingUpload(
|
|
|
1610
1844
|
}
|
|
1611
1845
|
}
|
|
1612
1846
|
|
|
1847
|
+
async function interruptRecordingUpload(
|
|
1848
|
+
serverUrl: string,
|
|
1849
|
+
recordingId: string,
|
|
1850
|
+
detail: string,
|
|
1851
|
+
authToken?: string,
|
|
1852
|
+
attemptId?: string,
|
|
1853
|
+
uploadGenerationId?: string,
|
|
1854
|
+
): Promise<void> {
|
|
1855
|
+
let lastError: unknown = null;
|
|
1856
|
+
for (let attempt = 1; attempt <= CHUNK_UPLOAD_MAX_ATTEMPTS; attempt += 1) {
|
|
1857
|
+
try {
|
|
1858
|
+
const res = await fetch(
|
|
1859
|
+
`${serverUrl.replace(/\/+$/, "")}/api/uploads/${recordingId}/interrupt`,
|
|
1860
|
+
{
|
|
1861
|
+
method: "POST",
|
|
1862
|
+
headers: buildRetryHeaders("application/json", authToken),
|
|
1863
|
+
credentials: "include",
|
|
1864
|
+
body: JSON.stringify({
|
|
1865
|
+
detail,
|
|
1866
|
+
...(attemptId ? { attemptId } : {}),
|
|
1867
|
+
...(uploadGenerationId ? { uploadGenerationId } : {}),
|
|
1868
|
+
}),
|
|
1869
|
+
},
|
|
1870
|
+
);
|
|
1871
|
+
if (res.ok) {
|
|
1872
|
+
console.info("[clips-recorder] upload interruption recorded", {
|
|
1873
|
+
recordingId,
|
|
1874
|
+
attempt,
|
|
1875
|
+
});
|
|
1876
|
+
return;
|
|
1877
|
+
}
|
|
1878
|
+
const body = await res.text().catch(() => "");
|
|
1879
|
+
lastError = new Error(
|
|
1880
|
+
`Upload interruption failed (${res.status}): ${body.slice(0, 200)}`,
|
|
1881
|
+
);
|
|
1882
|
+
if (!isRetriableChunkStatus(res.status)) break;
|
|
1883
|
+
} catch (err) {
|
|
1884
|
+
lastError = err;
|
|
1885
|
+
}
|
|
1886
|
+
if (attempt < CHUNK_UPLOAD_MAX_ATTEMPTS) {
|
|
1887
|
+
await wait(CHUNK_UPLOAD_RETRY_BASE_MS * 2 ** (attempt - 1));
|
|
1888
|
+
}
|
|
1889
|
+
}
|
|
1890
|
+
console.warn(
|
|
1891
|
+
"[clips-recorder] upload interruption could not be recorded:",
|
|
1892
|
+
lastError,
|
|
1893
|
+
);
|
|
1894
|
+
}
|
|
1895
|
+
|
|
1613
1896
|
async function trashRecording(
|
|
1614
1897
|
serverUrl: string,
|
|
1615
1898
|
recordingId: string,
|
|
@@ -2427,10 +2710,11 @@ async function tryStartRewindFullscreenRecording(
|
|
|
2427
2710
|
) {
|
|
2428
2711
|
return { recordingId: id, viewUrl };
|
|
2429
2712
|
}
|
|
2430
|
-
await
|
|
2713
|
+
await interruptRecordingUpload(
|
|
2431
2714
|
params.serverUrl,
|
|
2432
2715
|
id,
|
|
2433
2716
|
err instanceof Error ? err.message : String(err),
|
|
2717
|
+
params.authToken,
|
|
2434
2718
|
);
|
|
2435
2719
|
throw err;
|
|
2436
2720
|
}
|
|
@@ -3101,10 +3385,11 @@ async function startNativeFullscreenRecording(
|
|
|
3101
3385
|
) {
|
|
3102
3386
|
return { recordingId: id, viewUrl };
|
|
3103
3387
|
}
|
|
3104
|
-
await
|
|
3388
|
+
await interruptRecordingUpload(
|
|
3105
3389
|
params.serverUrl,
|
|
3106
3390
|
id,
|
|
3107
3391
|
err instanceof Error ? err.message : String(err),
|
|
3392
|
+
params.authToken,
|
|
3108
3393
|
);
|
|
3109
3394
|
throw err;
|
|
3110
3395
|
}
|
|
@@ -4453,7 +4738,12 @@ async function startRecordingInner(
|
|
|
4453
4738
|
await markBrowserRecordingBackupError(id, failed.message).catch(
|
|
4454
4739
|
() => {},
|
|
4455
4740
|
);
|
|
4456
|
-
await
|
|
4741
|
+
await interruptRecordingUpload(
|
|
4742
|
+
params.serverUrl,
|
|
4743
|
+
id,
|
|
4744
|
+
failed.message,
|
|
4745
|
+
params.authToken,
|
|
4746
|
+
);
|
|
4457
4747
|
} finally {
|
|
4458
4748
|
await clearRecordingState();
|
|
4459
4749
|
await publishFinalizingResult({
|
|
@@ -4559,7 +4849,12 @@ async function startRecordingInner(
|
|
|
4559
4849
|
await markBrowserRecordingBackupError(id, error.message).catch(
|
|
4560
4850
|
() => {},
|
|
4561
4851
|
);
|
|
4562
|
-
await
|
|
4852
|
+
await interruptRecordingUpload(
|
|
4853
|
+
params.serverUrl,
|
|
4854
|
+
id,
|
|
4855
|
+
error.message,
|
|
4856
|
+
params.authToken,
|
|
4857
|
+
);
|
|
4563
4858
|
throw error;
|
|
4564
4859
|
}
|
|
4565
4860
|
await deleteBrowserRecordingBackup(id).catch((err) => {
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
export type UploadResumeResponse =
|
|
2
|
+
| {
|
|
3
|
+
resumable: true;
|
|
4
|
+
recoveryEnabled: boolean;
|
|
5
|
+
status: string;
|
|
6
|
+
uploadMode: "streaming" | "buffered";
|
|
7
|
+
attemptId: string;
|
|
8
|
+
uploadGenerationId?: string;
|
|
9
|
+
bytesReceived: number;
|
|
10
|
+
nextChunkIndex: number;
|
|
11
|
+
}
|
|
12
|
+
| {
|
|
13
|
+
resumable: false;
|
|
14
|
+
recoveryEnabled: boolean;
|
|
15
|
+
status: string | null;
|
|
16
|
+
failureReason?: string | null;
|
|
17
|
+
videoUrl?: string | null;
|
|
18
|
+
reason?: string;
|
|
19
|
+
attemptId?: string;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export type StreamingRecoveryPlan =
|
|
23
|
+
| {
|
|
24
|
+
action: "resume";
|
|
25
|
+
bytesReceived: number;
|
|
26
|
+
nextChunkIndex: number;
|
|
27
|
+
progress: number;
|
|
28
|
+
}
|
|
29
|
+
| { action: "reconcile"; status: "ready" | "processing" }
|
|
30
|
+
| { action: "restart"; reason: string };
|
|
31
|
+
|
|
32
|
+
export interface StreamingReplayRequest {
|
|
33
|
+
index: number;
|
|
34
|
+
start: number;
|
|
35
|
+
end: number;
|
|
36
|
+
final: boolean;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function retryAttemptIdAfterRestartSignal(
|
|
40
|
+
attemptId: string | undefined,
|
|
41
|
+
recoveryEnabled: unknown,
|
|
42
|
+
): string | undefined {
|
|
43
|
+
return recoveryEnabled === false ? undefined : attemptId;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function retryAttemptIdAfterResumeResponse(
|
|
47
|
+
attemptId: string | undefined,
|
|
48
|
+
response: UploadResumeResponse,
|
|
49
|
+
): string | undefined {
|
|
50
|
+
return response.resumable && response.attemptId === attemptId
|
|
51
|
+
? attemptId
|
|
52
|
+
: undefined;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function buildStreamingReplayPlan(input: {
|
|
56
|
+
localBytes: number;
|
|
57
|
+
chunkBytes: number;
|
|
58
|
+
bytesReceived: number;
|
|
59
|
+
nextChunkIndex: number;
|
|
60
|
+
}): StreamingReplayRequest[] {
|
|
61
|
+
const { localBytes, chunkBytes, bytesReceived, nextChunkIndex } = input;
|
|
62
|
+
const fullChunks = Math.floor(localBytes / chunkBytes);
|
|
63
|
+
const requests: StreamingReplayRequest[] = [];
|
|
64
|
+
let offset = bytesReceived;
|
|
65
|
+
for (let index = nextChunkIndex; index < fullChunks; index += 1) {
|
|
66
|
+
requests.push({
|
|
67
|
+
index,
|
|
68
|
+
start: offset,
|
|
69
|
+
end: offset + chunkBytes,
|
|
70
|
+
final: false,
|
|
71
|
+
});
|
|
72
|
+
offset += chunkBytes;
|
|
73
|
+
}
|
|
74
|
+
requests.push({
|
|
75
|
+
index: fullChunks,
|
|
76
|
+
start: offset,
|
|
77
|
+
end: localBytes,
|
|
78
|
+
final: true,
|
|
79
|
+
});
|
|
80
|
+
return requests;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function planStreamingRecovery(input: {
|
|
84
|
+
response: UploadResumeResponse;
|
|
85
|
+
localBytes: number;
|
|
86
|
+
chunkBytes: number;
|
|
87
|
+
}): StreamingRecoveryPlan {
|
|
88
|
+
const { response, localBytes, chunkBytes } = input;
|
|
89
|
+
if (!response.recoveryEnabled) {
|
|
90
|
+
return { action: "restart", reason: "resumable retry is disabled" };
|
|
91
|
+
}
|
|
92
|
+
if (response.status === "ready" || response.status === "processing") {
|
|
93
|
+
return { action: "reconcile", status: response.status };
|
|
94
|
+
}
|
|
95
|
+
if (!response.resumable) {
|
|
96
|
+
return {
|
|
97
|
+
action: "restart",
|
|
98
|
+
reason: response.reason ?? "server session is unavailable",
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
if (response.uploadMode !== "streaming") {
|
|
102
|
+
return { action: "restart", reason: "server session is not streaming" };
|
|
103
|
+
}
|
|
104
|
+
if (!response.attemptId) {
|
|
105
|
+
return { action: "restart", reason: "server retry token is missing" };
|
|
106
|
+
}
|
|
107
|
+
const { bytesReceived, nextChunkIndex } = response;
|
|
108
|
+
if (
|
|
109
|
+
!Number.isSafeInteger(bytesReceived) ||
|
|
110
|
+
bytesReceived < 0 ||
|
|
111
|
+
bytesReceived > localBytes
|
|
112
|
+
) {
|
|
113
|
+
return { action: "restart", reason: "server byte offset is invalid" };
|
|
114
|
+
}
|
|
115
|
+
if (
|
|
116
|
+
!Number.isSafeInteger(nextChunkIndex) ||
|
|
117
|
+
nextChunkIndex < 0 ||
|
|
118
|
+
bytesReceived % chunkBytes !== 0 ||
|
|
119
|
+
nextChunkIndex !== bytesReceived / chunkBytes
|
|
120
|
+
) {
|
|
121
|
+
return { action: "restart", reason: "server chunk offset is inconsistent" };
|
|
122
|
+
}
|
|
123
|
+
return {
|
|
124
|
+
action: "resume",
|
|
125
|
+
bytesReceived,
|
|
126
|
+
nextChunkIndex,
|
|
127
|
+
progress: localBytes > 0 ? bytesReceived / localBytes : 0,
|
|
128
|
+
};
|
|
129
|
+
}
|