@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
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import { getDbExec, isPostgres } from "@agent-native/core/db";
|
|
15
|
-
import { and, eq, inArray, sql } from "drizzle-orm";
|
|
15
|
+
import { and, eq, inArray, isNull, sql } from "drizzle-orm";
|
|
16
16
|
|
|
17
17
|
import { getDb, schema } from "../db/index.js";
|
|
18
18
|
|
|
@@ -37,6 +37,7 @@ export type UploadLeaseResult =
|
|
|
37
37
|
| { held: true }
|
|
38
38
|
| {
|
|
39
39
|
held: false;
|
|
40
|
+
staleAttempt: boolean;
|
|
40
41
|
status: string | null;
|
|
41
42
|
failureReason: string | null;
|
|
42
43
|
videoUrl: string | null;
|
|
@@ -53,7 +54,12 @@ export type UploadLeaseResult =
|
|
|
53
54
|
*/
|
|
54
55
|
export async function renewUploadLease(
|
|
55
56
|
recordingId: string,
|
|
56
|
-
options: {
|
|
57
|
+
options: {
|
|
58
|
+
now?: number;
|
|
59
|
+
uploadProgress?: number;
|
|
60
|
+
attemptId?: string | null;
|
|
61
|
+
generationId?: string | null;
|
|
62
|
+
} = {},
|
|
57
63
|
): Promise<UploadLeaseResult> {
|
|
58
64
|
const now = options.now ?? Date.now();
|
|
59
65
|
const held = await getDb()
|
|
@@ -72,6 +78,23 @@ export async function renewUploadLease(
|
|
|
72
78
|
and(
|
|
73
79
|
eq(schema.recordings.id, recordingId),
|
|
74
80
|
inArray(schema.recordings.status, [...IN_PROGRESS_STATUSES]),
|
|
81
|
+
...(options.attemptId === undefined
|
|
82
|
+
? []
|
|
83
|
+
: [
|
|
84
|
+
options.attemptId === null
|
|
85
|
+
? isNull(schema.recordings.uploadAttemptId)
|
|
86
|
+
: eq(schema.recordings.uploadAttemptId, options.attemptId),
|
|
87
|
+
]),
|
|
88
|
+
...(options.generationId === undefined
|
|
89
|
+
? []
|
|
90
|
+
: [
|
|
91
|
+
options.generationId === null
|
|
92
|
+
? isNull(schema.recordings.uploadGenerationId)
|
|
93
|
+
: eq(
|
|
94
|
+
schema.recordings.uploadGenerationId,
|
|
95
|
+
options.generationId,
|
|
96
|
+
),
|
|
97
|
+
]),
|
|
75
98
|
),
|
|
76
99
|
)
|
|
77
100
|
.returning({ id: schema.recordings.id });
|
|
@@ -87,12 +110,23 @@ export async function renewUploadLease(
|
|
|
87
110
|
videoUrl: schema.recordings.videoUrl,
|
|
88
111
|
videoSizeBytes: schema.recordings.videoSizeBytes,
|
|
89
112
|
durationMs: schema.recordings.durationMs,
|
|
113
|
+
uploadAttemptId: schema.recordings.uploadAttemptId,
|
|
114
|
+
uploadGenerationId: schema.recordings.uploadGenerationId,
|
|
90
115
|
})
|
|
91
116
|
.from(schema.recordings)
|
|
92
117
|
.where(eq(schema.recordings.id, recordingId));
|
|
93
118
|
|
|
94
119
|
return {
|
|
95
120
|
held: false,
|
|
121
|
+
staleAttempt:
|
|
122
|
+
(options.attemptId !== undefined || options.generationId !== undefined) &&
|
|
123
|
+
IN_PROGRESS_STATUSES.includes(
|
|
124
|
+
row?.status as (typeof IN_PROGRESS_STATUSES)[number],
|
|
125
|
+
) &&
|
|
126
|
+
((options.attemptId !== undefined &&
|
|
127
|
+
row?.uploadAttemptId !== options.attemptId) ||
|
|
128
|
+
(options.generationId !== undefined &&
|
|
129
|
+
row?.uploadGenerationId !== options.generationId)),
|
|
96
130
|
status: row?.status ?? null,
|
|
97
131
|
failureReason: row?.failureReason ?? null,
|
|
98
132
|
videoUrl: row?.videoUrl ?? null,
|
|
@@ -106,6 +140,7 @@ export interface ReapedUpload {
|
|
|
106
140
|
ownerEmail: string;
|
|
107
141
|
status: string;
|
|
108
142
|
leaseExpiresAt: string;
|
|
143
|
+
uploadGenerationId: string | null;
|
|
109
144
|
}
|
|
110
145
|
|
|
111
146
|
export interface ReapResult {
|
|
@@ -137,7 +172,7 @@ export async function reapExpiredUploads(
|
|
|
137
172
|
|
|
138
173
|
// guard:allow-unscoped — system upload reaper, owner-agnostic by design.
|
|
139
174
|
const probe = await exec.execute({
|
|
140
|
-
sql: `SELECT id, owner_email, status, upload_lease_expires_at
|
|
175
|
+
sql: `SELECT id, owner_email, status, upload_lease_expires_at, upload_generation_id
|
|
141
176
|
FROM recordings
|
|
142
177
|
WHERE status IN ('uploading', 'processing')
|
|
143
178
|
AND upload_lease_expires_at IS NOT NULL
|
|
@@ -154,6 +189,10 @@ export async function reapExpiredUploads(
|
|
|
154
189
|
ownerEmail: String(row.owner_email ?? ""),
|
|
155
190
|
status: String(row.status ?? ""),
|
|
156
191
|
leaseExpiresAt: String(row.upload_lease_expires_at ?? ""),
|
|
192
|
+
uploadGenerationId:
|
|
193
|
+
typeof row.upload_generation_id === "string"
|
|
194
|
+
? row.upload_generation_id
|
|
195
|
+
: null,
|
|
157
196
|
}));
|
|
158
197
|
|
|
159
198
|
let failed = 0;
|
|
@@ -184,9 +223,16 @@ export async function reapExpiredUploads(
|
|
|
184
223
|
failed = terminated.size;
|
|
185
224
|
|
|
186
225
|
for (const id of terminated) {
|
|
226
|
+
const generationId = expired.find(
|
|
227
|
+
(row) => row.id === id,
|
|
228
|
+
)?.uploadGenerationId;
|
|
187
229
|
await exec.execute({
|
|
188
230
|
sql: `DELETE FROM application_state WHERE key = ${p(1)}`,
|
|
189
|
-
args: [
|
|
231
|
+
args: [
|
|
232
|
+
generationId
|
|
233
|
+
? `resumable-session-${id}-${generationId}`
|
|
234
|
+
: `resumable-session-${id}`,
|
|
235
|
+
],
|
|
190
236
|
});
|
|
191
237
|
}
|
|
192
238
|
}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generate a filmstrip sprite — one JPEG holding a grid of evenly-spaced video
|
|
3
|
+
* frames — so the editor timeline renders thumbnails as a single cached image
|
|
4
|
+
* request instead of decoding the video in the browser.
|
|
5
|
+
*
|
|
6
|
+
* Why a sprite rather than per-frame files: one ffmpeg pass, one upload, one
|
|
7
|
+
* HTTP request, and the grid geometry is enough for CSS `background-position`
|
|
8
|
+
* to address any cell. The browser fallback in `app/lib/video-filmstrip.ts`
|
|
9
|
+
* needs one seek per frame and cannot read cross-origin media at all.
|
|
10
|
+
*
|
|
11
|
+
* Three constraints worth keeping in mind:
|
|
12
|
+
*
|
|
13
|
+
* - Cells are padded to an exact size (`force_original_aspect_ratio=decrease`
|
|
14
|
+
* plus `pad`) so the returned grid geometry is exact without probing the
|
|
15
|
+
* source. Letterboxed bars on odd aspect ratios are the deliberate cost of
|
|
16
|
+
* not having to decode the output to learn its dimensions.
|
|
17
|
+
* - Sampling starts half a cell in, matching the browser fallback: cell `i`
|
|
18
|
+
* shows the midpoint of the time slot it occupies, not its leading edge.
|
|
19
|
+
* - Input arrives as bytes and is written to a temp file, like every other
|
|
20
|
+
* ffmpeg path here. That means the whole file is buffered in memory by the
|
|
21
|
+
* caller, which is the practical ceiling on this approach for long
|
|
22
|
+
* recordings on small hosts.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
26
|
+
import { tmpdir } from "node:os";
|
|
27
|
+
import { join } from "node:path";
|
|
28
|
+
|
|
29
|
+
import { isFfmpegAvailable, runFfmpeg, withRemuxSlot } from "./video-remux.js";
|
|
30
|
+
|
|
31
|
+
const SPRITE_TIMEOUT_MS = 90_000;
|
|
32
|
+
|
|
33
|
+
export const DEFAULT_FILMSTRIP_FRAME_COUNT = 40;
|
|
34
|
+
// A sprite is decoded whole by the browser, so cap the grid: 120 cells at the
|
|
35
|
+
// default size is already a ~1600x1080 image.
|
|
36
|
+
export const MAX_FILMSTRIP_FRAME_COUNT = 120;
|
|
37
|
+
export const DEFAULT_FILMSTRIP_FRAME_WIDTH = 160;
|
|
38
|
+
export const DEFAULT_FILMSTRIP_FRAME_HEIGHT = 90;
|
|
39
|
+
export const DEFAULT_FILMSTRIP_COLUMNS = 10;
|
|
40
|
+
|
|
41
|
+
export type FilmstripSpriteStatus =
|
|
42
|
+
| "generated"
|
|
43
|
+
| "skipped-no-ffmpeg"
|
|
44
|
+
| "skipped-no-duration"
|
|
45
|
+
| "skipped-no-media"
|
|
46
|
+
| "failed-ffmpeg"
|
|
47
|
+
| "failed-empty-output";
|
|
48
|
+
|
|
49
|
+
/** Everything the UI needs to address one cell of the sprite. */
|
|
50
|
+
export interface FilmstripSpriteGrid {
|
|
51
|
+
frameCount: number;
|
|
52
|
+
columns: number;
|
|
53
|
+
rows: number;
|
|
54
|
+
frameWidth: number;
|
|
55
|
+
frameHeight: number;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface FilmstripSpriteResult {
|
|
59
|
+
status: FilmstripSpriteStatus;
|
|
60
|
+
/** Present only when `status === "generated"`. */
|
|
61
|
+
sprite?: { bytes: Uint8Array; grid: FilmstripSpriteGrid };
|
|
62
|
+
detail?: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface GenerateFilmstripSpriteInput {
|
|
66
|
+
mediaBytes: Uint8Array;
|
|
67
|
+
durationMs: number;
|
|
68
|
+
frameCount?: number;
|
|
69
|
+
frameWidth?: number;
|
|
70
|
+
frameHeight?: number;
|
|
71
|
+
columns?: number;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Lay `frameCount` cells out into a grid no wider than `columns`. Exported so
|
|
76
|
+
* callers can compute the same geometry the sprite will have without
|
|
77
|
+
* regenerating it.
|
|
78
|
+
*/
|
|
79
|
+
export function filmstripGrid(input: {
|
|
80
|
+
frameCount: number;
|
|
81
|
+
columns: number;
|
|
82
|
+
frameWidth: number;
|
|
83
|
+
frameHeight: number;
|
|
84
|
+
}): FilmstripSpriteGrid {
|
|
85
|
+
const frameCount = Math.min(
|
|
86
|
+
MAX_FILMSTRIP_FRAME_COUNT,
|
|
87
|
+
Math.max(1, Math.floor(input.frameCount)),
|
|
88
|
+
);
|
|
89
|
+
const columns = Math.max(1, Math.min(input.columns, frameCount));
|
|
90
|
+
return {
|
|
91
|
+
frameCount,
|
|
92
|
+
columns,
|
|
93
|
+
rows: Math.ceil(frameCount / columns),
|
|
94
|
+
frameWidth: input.frameWidth,
|
|
95
|
+
frameHeight: input.frameHeight,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Build the ffmpeg filter chain that samples `frameCount` frames across
|
|
101
|
+
* `durationMs` and tiles them into `grid`. Exported for tests: the sampling
|
|
102
|
+
* offset and rate are the parts most likely to drift from the browser
|
|
103
|
+
* fallback's expectations.
|
|
104
|
+
*/
|
|
105
|
+
export function filmstripSpriteFilter(input: {
|
|
106
|
+
durationMs: number;
|
|
107
|
+
grid: FilmstripSpriteGrid;
|
|
108
|
+
}): { seekSeconds: string; filter: string } {
|
|
109
|
+
const { durationMs, grid } = input;
|
|
110
|
+
const cellMs = durationMs / grid.frameCount;
|
|
111
|
+
const fps = grid.frameCount / (durationMs / 1000);
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
// Half a cell in, so each tile is the midpoint of its slot.
|
|
115
|
+
seekSeconds: (cellMs / 2 / 1000).toFixed(6),
|
|
116
|
+
filter: [
|
|
117
|
+
`fps=${fps.toFixed(6)}`,
|
|
118
|
+
`scale=${grid.frameWidth}:${grid.frameHeight}:force_original_aspect_ratio=decrease`,
|
|
119
|
+
`pad=${grid.frameWidth}:${grid.frameHeight}:-1:-1:color=black`,
|
|
120
|
+
`tile=${grid.columns}x${grid.rows}`,
|
|
121
|
+
].join(","),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export async function generateFilmstripSprite(
|
|
126
|
+
input: GenerateFilmstripSpriteInput,
|
|
127
|
+
): Promise<FilmstripSpriteResult> {
|
|
128
|
+
const {
|
|
129
|
+
mediaBytes,
|
|
130
|
+
durationMs,
|
|
131
|
+
frameCount = DEFAULT_FILMSTRIP_FRAME_COUNT,
|
|
132
|
+
frameWidth = DEFAULT_FILMSTRIP_FRAME_WIDTH,
|
|
133
|
+
frameHeight = DEFAULT_FILMSTRIP_FRAME_HEIGHT,
|
|
134
|
+
columns = DEFAULT_FILMSTRIP_COLUMNS,
|
|
135
|
+
} = input;
|
|
136
|
+
|
|
137
|
+
if (mediaBytes.byteLength === 0) {
|
|
138
|
+
return { status: "skipped-no-media" };
|
|
139
|
+
}
|
|
140
|
+
if (!Number.isFinite(durationMs) || durationMs <= 0) {
|
|
141
|
+
return { status: "skipped-no-duration" };
|
|
142
|
+
}
|
|
143
|
+
if (!isFfmpegAvailable()) {
|
|
144
|
+
return {
|
|
145
|
+
status: "skipped-no-ffmpeg",
|
|
146
|
+
detail:
|
|
147
|
+
"No ffmpeg binary available; the editor falls back to browser-side frame extraction.",
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const grid = filmstripGrid({ frameCount, columns, frameWidth, frameHeight });
|
|
152
|
+
const { seekSeconds, filter } = filmstripSpriteFilter({ durationMs, grid });
|
|
153
|
+
|
|
154
|
+
const dir = await mkdtemp(join(tmpdir(), "clips-filmstrip-"));
|
|
155
|
+
const inputPath = join(dir, "input.media");
|
|
156
|
+
const outputPath = join(dir, "sprite.jpg");
|
|
157
|
+
|
|
158
|
+
try {
|
|
159
|
+
await writeFile(inputPath, mediaBytes);
|
|
160
|
+
await withRemuxSlot(() =>
|
|
161
|
+
runFfmpeg(
|
|
162
|
+
[
|
|
163
|
+
"-hide_banner",
|
|
164
|
+
"-loglevel",
|
|
165
|
+
"error",
|
|
166
|
+
"-nostdin",
|
|
167
|
+
"-y",
|
|
168
|
+
// Input seeking: cheap, and puts the first sample at a cell midpoint.
|
|
169
|
+
"-ss",
|
|
170
|
+
seekSeconds,
|
|
171
|
+
"-i",
|
|
172
|
+
inputPath,
|
|
173
|
+
"-an",
|
|
174
|
+
"-sn",
|
|
175
|
+
"-vf",
|
|
176
|
+
filter,
|
|
177
|
+
// The tile filter emits the finished grid as a single frame.
|
|
178
|
+
"-frames:v",
|
|
179
|
+
"1",
|
|
180
|
+
"-q:v",
|
|
181
|
+
"4",
|
|
182
|
+
"-f",
|
|
183
|
+
"image2",
|
|
184
|
+
outputPath,
|
|
185
|
+
],
|
|
186
|
+
{ timeoutMs: SPRITE_TIMEOUT_MS, label: "filmstrip sprite" },
|
|
187
|
+
),
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
const bytes = new Uint8Array(await readFile(outputPath));
|
|
191
|
+
if (bytes.byteLength === 0) {
|
|
192
|
+
return {
|
|
193
|
+
status: "failed-empty-output",
|
|
194
|
+
detail: "ffmpeg reported success but wrote an empty sprite",
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return { status: "generated", sprite: { bytes, grid } };
|
|
199
|
+
} catch (err) {
|
|
200
|
+
return {
|
|
201
|
+
status: "failed-ffmpeg",
|
|
202
|
+
detail: err instanceof Error ? err.message : String(err),
|
|
203
|
+
};
|
|
204
|
+
} finally {
|
|
205
|
+
await rm(dir, { recursive: true, force: true }).catch(() => {});
|
|
206
|
+
}
|
|
207
|
+
}
|
|
@@ -125,7 +125,11 @@ function startsWithMagic(bytes: Uint8Array, magic: number[]): boolean {
|
|
|
125
125
|
return true;
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
-
async function runFfmpeg(
|
|
128
|
+
export async function runFfmpeg(
|
|
129
|
+
args: string[],
|
|
130
|
+
options: { timeoutMs?: number; label?: string } = {},
|
|
131
|
+
): Promise<void> {
|
|
132
|
+
const { timeoutMs = REMUX_TIMEOUT_MS, label = "remux" } = options;
|
|
129
133
|
await new Promise<void>((resolve, reject) => {
|
|
130
134
|
const child = spawn(ffmpegCommand(), args, {
|
|
131
135
|
stdio: ["ignore", "ignore", "pipe"],
|
|
@@ -133,8 +137,8 @@ async function runFfmpeg(args: string[]): Promise<void> {
|
|
|
133
137
|
let stderr = "";
|
|
134
138
|
const timeout = setTimeout(() => {
|
|
135
139
|
child.kill("SIGKILL");
|
|
136
|
-
reject(new Error(`ffmpeg
|
|
137
|
-
},
|
|
140
|
+
reject(new Error(`ffmpeg ${label} timed out\n${stderr}`));
|
|
141
|
+
}, timeoutMs);
|
|
138
142
|
|
|
139
143
|
child.stderr?.on("data", (chunk: Buffer) => {
|
|
140
144
|
stderr = (stderr + chunk.toString("utf8")).slice(-STDERR_LIMIT);
|
|
@@ -244,7 +248,12 @@ export async function probeHasAudioStream(
|
|
|
244
248
|
}
|
|
245
249
|
}
|
|
246
250
|
|
|
247
|
-
|
|
251
|
+
/**
|
|
252
|
+
* Bound concurrent ffmpeg invocations process-wide. Every ffmpeg caller must go
|
|
253
|
+
* through this, including non-remux jobs — the limit protects the box, not the
|
|
254
|
+
* remux path specifically.
|
|
255
|
+
*/
|
|
256
|
+
export async function withRemuxSlot<T>(fn: () => Promise<T>): Promise<T> {
|
|
248
257
|
if (activeRemuxes >= MAX_CONCURRENT_REMUXES) {
|
|
249
258
|
await new Promise<void>((resolve) => remuxWaiters.push(resolve));
|
|
250
259
|
}
|
|
@@ -193,6 +193,12 @@ const migrations = runMigrations(
|
|
|
193
193
|
description TEXT NOT NULL DEFAULT '',
|
|
194
194
|
thumbnail_url TEXT,
|
|
195
195
|
animated_thumbnail_url TEXT,
|
|
196
|
+
filmstrip_url TEXT,
|
|
197
|
+
filmstrip_frame_count INTEGER NOT NULL DEFAULT 0,
|
|
198
|
+
filmstrip_columns INTEGER NOT NULL DEFAULT 0,
|
|
199
|
+
filmstrip_rows INTEGER NOT NULL DEFAULT 0,
|
|
200
|
+
filmstrip_frame_width INTEGER NOT NULL DEFAULT 0,
|
|
201
|
+
filmstrip_frame_height INTEGER NOT NULL DEFAULT 0,
|
|
196
202
|
duration_ms INTEGER NOT NULL DEFAULT 0,
|
|
197
203
|
video_url TEXT,
|
|
198
204
|
video_format TEXT NOT NULL DEFAULT 'webm',
|
|
@@ -899,6 +905,16 @@ const migrations = runMigrations(
|
|
|
899
905
|
`UPDATE recordings SET upload_lease_expires_at = '${uploadLeaseExpiry()}' WHERE upload_lease_expires_at IS NULL AND status IN ('uploading', 'processing')`,
|
|
900
906
|
].join("; "),
|
|
901
907
|
},
|
|
908
|
+
{
|
|
909
|
+
version: 54,
|
|
910
|
+
name: "recording-upload-attempt-fence",
|
|
911
|
+
sql: `ALTER TABLE recordings ADD COLUMN IF NOT EXISTS upload_attempt_id TEXT`,
|
|
912
|
+
},
|
|
913
|
+
{
|
|
914
|
+
version: 55,
|
|
915
|
+
name: "recording-upload-generation-fence",
|
|
916
|
+
sql: `ALTER TABLE recordings ADD COLUMN IF NOT EXISTS upload_generation_id TEXT`,
|
|
917
|
+
},
|
|
902
918
|
],
|
|
903
919
|
{ table: "clips_migrations" },
|
|
904
920
|
);
|
|
@@ -66,6 +66,7 @@ export default defineEventHandler(async (event: H3Event) => {
|
|
|
66
66
|
ownerEmail: schema.recordings.ownerEmail,
|
|
67
67
|
orgId: schema.recordings.orgId,
|
|
68
68
|
status: schema.recordings.status,
|
|
69
|
+
uploadGenerationId: schema.recordings.uploadGenerationId,
|
|
69
70
|
})
|
|
70
71
|
.from(schema.recordings)
|
|
71
72
|
.where(eq(schema.recordings.id, recordingId))
|
|
@@ -120,6 +121,9 @@ export default defineEventHandler(async (event: H3Event) => {
|
|
|
120
121
|
const result = await finalizeRecording.run({
|
|
121
122
|
id: recordingId,
|
|
122
123
|
mediaVerificationRetryAttempt: retryAttempt ?? 1,
|
|
124
|
+
...(recording.uploadGenerationId
|
|
125
|
+
? { uploadGenerationId: recording.uploadGenerationId }
|
|
126
|
+
: {}),
|
|
123
127
|
});
|
|
124
128
|
return { ok: true, kind, result };
|
|
125
129
|
}
|
|
@@ -6,14 +6,13 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import {
|
|
9
|
-
|
|
9
|
+
compareAndSetManyAppState,
|
|
10
10
|
readAppState,
|
|
11
11
|
writeAppState,
|
|
12
|
-
deleteAppStateByPrefix,
|
|
13
12
|
} from "@agent-native/core/application-state";
|
|
14
13
|
import { runWithRequestContext } from "@agent-native/core/server";
|
|
15
14
|
import { isStoredButUnservableFinalizeError } from "@shared/finalize-recovery.js";
|
|
16
|
-
import { and, eq } from "drizzle-orm";
|
|
15
|
+
import { and, eq, isNull } from "drizzle-orm";
|
|
17
16
|
import {
|
|
18
17
|
defineEventHandler,
|
|
19
18
|
getRouterParam,
|
|
@@ -24,6 +23,7 @@ import {
|
|
|
24
23
|
|
|
25
24
|
import { getDb, schema } from "../../../../db/index.js";
|
|
26
25
|
import { mediaVerificationStateKey } from "../../../../lib/media-verification-state.js";
|
|
26
|
+
import { deleteRecordingChunks } from "../../../../lib/recording-upload-state.js";
|
|
27
27
|
import {
|
|
28
28
|
getEventOwnerContext,
|
|
29
29
|
ownerEmailMatches,
|
|
@@ -44,11 +44,25 @@ export default defineEventHandler(async (event: H3Event) => {
|
|
|
44
44
|
const { userEmail: ownerEmail, orgId } = await getEventOwnerContext(event);
|
|
45
45
|
const body = (await readBody(event).catch(() => null)) as {
|
|
46
46
|
reason?: unknown;
|
|
47
|
+
attemptId?: unknown;
|
|
48
|
+
uploadGenerationId?: unknown;
|
|
47
49
|
} | null;
|
|
48
50
|
const failureReason =
|
|
49
51
|
typeof body?.reason === "string" && body.reason.trim()
|
|
50
52
|
? body.reason.trim().slice(0, 1000)
|
|
51
53
|
: "Upload aborted by user";
|
|
54
|
+
const requestedAttemptId =
|
|
55
|
+
typeof body?.attemptId === "string" &&
|
|
56
|
+
body.attemptId.length > 0 &&
|
|
57
|
+
body.attemptId.length <= 128
|
|
58
|
+
? body.attemptId
|
|
59
|
+
: null;
|
|
60
|
+
const requestedGenerationId =
|
|
61
|
+
typeof body?.uploadGenerationId === "string" &&
|
|
62
|
+
body.uploadGenerationId.length > 0 &&
|
|
63
|
+
body.uploadGenerationId.length <= 128
|
|
64
|
+
? body.uploadGenerationId
|
|
65
|
+
: null;
|
|
52
66
|
|
|
53
67
|
return runWithRequestContext({ userEmail: ownerEmail, orgId }, async () => {
|
|
54
68
|
const db = getDb();
|
|
@@ -59,6 +73,8 @@ export default defineEventHandler(async (event: H3Event) => {
|
|
|
59
73
|
status: schema.recordings.status,
|
|
60
74
|
videoUrl: schema.recordings.videoUrl,
|
|
61
75
|
failureReason: schema.recordings.failureReason,
|
|
76
|
+
uploadAttemptId: schema.recordings.uploadAttemptId,
|
|
77
|
+
uploadGenerationId: schema.recordings.uploadGenerationId,
|
|
62
78
|
})
|
|
63
79
|
.from(schema.recordings)
|
|
64
80
|
.where(
|
|
@@ -73,17 +89,43 @@ export default defineEventHandler(async (event: H3Event) => {
|
|
|
73
89
|
return { error: "Recording not found" };
|
|
74
90
|
}
|
|
75
91
|
|
|
92
|
+
const existingAttemptId = existing.uploadAttemptId ?? null;
|
|
93
|
+
const existingGenerationId = existing.uploadGenerationId ?? null;
|
|
94
|
+
if (
|
|
95
|
+
requestedAttemptId !== existingAttemptId ||
|
|
96
|
+
requestedGenerationId !== existingGenerationId
|
|
97
|
+
) {
|
|
98
|
+
setResponseStatus(event, 409);
|
|
99
|
+
return {
|
|
100
|
+
error: "A newer upload retry is already active.",
|
|
101
|
+
staleAttempt: true,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
76
105
|
if (existing.status === "ready" && existing.videoUrl) {
|
|
77
106
|
return { ok: true, recordingId, alreadyReady: true, chunksCleared: 0 };
|
|
78
107
|
}
|
|
79
108
|
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
109
|
+
const uploadStateKey = `recording-upload-${recordingId}`;
|
|
110
|
+
const verificationStateKey = mediaVerificationStateKey(recordingId);
|
|
111
|
+
const [existingUploadStateRaw, existingVerificationStateRaw] =
|
|
112
|
+
await Promise.all([
|
|
113
|
+
readAppState(uploadStateKey),
|
|
114
|
+
readAppState(verificationStateKey),
|
|
115
|
+
]);
|
|
83
116
|
const existingUploadState =
|
|
84
117
|
existingUploadStateRaw && typeof existingUploadStateRaw === "object"
|
|
85
118
|
? (existingUploadStateRaw as Record<string, unknown>)
|
|
86
119
|
: {};
|
|
120
|
+
const existingUploadStateSnapshot =
|
|
121
|
+
existingUploadStateRaw && typeof existingUploadStateRaw === "object"
|
|
122
|
+
? (existingUploadStateRaw as Record<string, unknown>)
|
|
123
|
+
: null;
|
|
124
|
+
const existingVerificationStateSnapshot =
|
|
125
|
+
existingVerificationStateRaw &&
|
|
126
|
+
typeof existingVerificationStateRaw === "object"
|
|
127
|
+
? (existingVerificationStateRaw as Record<string, unknown>)
|
|
128
|
+
: null;
|
|
87
129
|
if (
|
|
88
130
|
existing.status === "processing" &&
|
|
89
131
|
existingUploadState.pendingMediaVerification === true
|
|
@@ -99,46 +141,87 @@ export default defineEventHandler(async (event: H3Event) => {
|
|
|
99
141
|
const preserveRecoveryState =
|
|
100
142
|
isStoredButUnservableFinalizeError(failureReason) ||
|
|
101
143
|
isStoredButUnservableFinalizeError(existing.failureReason);
|
|
102
|
-
|
|
144
|
+
let resumableSession = preserveRecoveryState
|
|
103
145
|
? null
|
|
104
|
-
: await getResumableSession(recordingId
|
|
105
|
-
|
|
106
|
-
// Already a terminal failure (e.g. a duplicate/retried abort call, or
|
|
107
|
-
// finalize's own failChunkAssembly already flipped it) — no-op unless a
|
|
108
|
-
// prior provider cleanup failure intentionally retained a resumable
|
|
109
|
-
// session for this retry.
|
|
110
|
-
if (
|
|
111
|
-
existing.status === "failed" &&
|
|
112
|
-
!preserveRecoveryState &&
|
|
113
|
-
!resumableSession
|
|
114
|
-
) {
|
|
115
|
-
return { ok: true, recordingId, alreadyFailed: true, chunksCleared: 0 };
|
|
116
|
-
}
|
|
146
|
+
: await getResumableSession(recordingId, existingGenerationId);
|
|
117
147
|
|
|
118
148
|
const now = new Date().toISOString();
|
|
119
|
-
await db
|
|
149
|
+
const aborted = await db
|
|
120
150
|
.update(schema.recordings)
|
|
121
151
|
.set({
|
|
122
152
|
status: "failed",
|
|
123
153
|
failureReason,
|
|
124
154
|
updatedAt: now,
|
|
125
155
|
})
|
|
126
|
-
.where(
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
156
|
+
.where(
|
|
157
|
+
and(
|
|
158
|
+
eq(schema.recordings.id, recordingId),
|
|
159
|
+
ownerEmailMatches(schema.recordings.ownerEmail, ownerEmail),
|
|
160
|
+
eq(schema.recordings.status, existing.status),
|
|
161
|
+
existingAttemptId === null
|
|
162
|
+
? isNull(schema.recordings.uploadAttemptId)
|
|
163
|
+
: eq(schema.recordings.uploadAttemptId, existingAttemptId),
|
|
164
|
+
existingGenerationId === null
|
|
165
|
+
? isNull(schema.recordings.uploadGenerationId)
|
|
166
|
+
: eq(schema.recordings.uploadGenerationId, existingGenerationId),
|
|
167
|
+
),
|
|
168
|
+
)
|
|
169
|
+
.returning({ id: schema.recordings.id });
|
|
170
|
+
|
|
171
|
+
if (aborted.length !== 1) {
|
|
172
|
+
setResponseStatus(event, 409);
|
|
173
|
+
return {
|
|
174
|
+
error: "A newer upload retry is already active.",
|
|
175
|
+
staleAttempt: true,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const auxiliaryStateUpdated = await compareAndSetManyAppState([
|
|
180
|
+
{
|
|
181
|
+
key: uploadStateKey,
|
|
182
|
+
expectedValue: existingUploadStateSnapshot,
|
|
183
|
+
nextValue: {
|
|
184
|
+
...existingUploadState,
|
|
185
|
+
recordingId,
|
|
186
|
+
status: "failed",
|
|
187
|
+
failureReason,
|
|
188
|
+
updatedAt: now,
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
...(existingVerificationStateSnapshot
|
|
192
|
+
? [
|
|
193
|
+
{
|
|
194
|
+
key: verificationStateKey,
|
|
195
|
+
expectedValue: existingVerificationStateSnapshot,
|
|
196
|
+
nextValue: null,
|
|
197
|
+
},
|
|
198
|
+
]
|
|
199
|
+
: []),
|
|
200
|
+
]);
|
|
201
|
+
if (!auxiliaryStateUpdated) {
|
|
202
|
+
console.info(
|
|
203
|
+
`[abort] upload state changed after abort claim; preserving replacement state for ${recordingId}`,
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Reset may have started this exact generation's provider session after
|
|
208
|
+
// our preflight read but before the abort claim. Re-read after the row CAS
|
|
209
|
+
// so either abort observes the late handle or reset observes the lost row
|
|
210
|
+
// claim and compensates it.
|
|
211
|
+
if (!preserveRecoveryState) {
|
|
212
|
+
resumableSession = await getResumableSession(
|
|
213
|
+
recordingId,
|
|
214
|
+
existingGenerationId,
|
|
215
|
+
);
|
|
216
|
+
}
|
|
138
217
|
|
|
139
218
|
const cleared = preserveRecoveryState
|
|
140
219
|
? 0
|
|
141
|
-
: await
|
|
220
|
+
: await deleteRecordingChunks(
|
|
221
|
+
ownerEmail,
|
|
222
|
+
recordingId,
|
|
223
|
+
existingGenerationId,
|
|
224
|
+
);
|
|
142
225
|
if (!preserveRecoveryState) {
|
|
143
226
|
if (resumableSession) {
|
|
144
227
|
const provider = await resolveResumableUploadProvider(
|
|
@@ -166,10 +249,14 @@ export default defineEventHandler(async (event: H3Event) => {
|
|
|
166
249
|
// retry can still address the multipart upload. Deleting it here would
|
|
167
250
|
// permanently orphan the provider-side session.
|
|
168
251
|
if (providerCleanupSucceeded) {
|
|
169
|
-
await deleteResumableSession(recordingId).catch(
|
|
252
|
+
await deleteResumableSession(recordingId, existingGenerationId).catch(
|
|
253
|
+
() => {},
|
|
254
|
+
);
|
|
170
255
|
}
|
|
171
256
|
} else {
|
|
172
|
-
await deleteResumableSession(recordingId).catch(
|
|
257
|
+
await deleteResumableSession(recordingId, existingGenerationId).catch(
|
|
258
|
+
() => {},
|
|
259
|
+
);
|
|
173
260
|
}
|
|
174
261
|
}
|
|
175
262
|
await writeAppState("refresh-signal", { ts: Date.now() });
|