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