@agent-native/core 0.134.0 → 0.135.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/corpus/README.md +3 -3
- package/corpus/core/CHANGELOG.md +113 -0
- package/corpus/core/docs/content/template-content-developers.mdx +1 -1
- 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-29-meeting-notes-no-longer-transcribe-the-other-side-twice-when.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/hooks/useMeetingTranscription.ts +37 -48
- package/corpus/templates/clips/desktop/src/lib/recorder.ts +331 -36
- package/corpus/templates/clips/desktop/src/lib/transcription-capture.ts +7 -5
- package/corpus/templates/clips/desktop/src/lib/transcription-engine.ts +182 -106
- package/corpus/templates/clips/desktop/src/lib/upload-recovery.ts +129 -0
- package/corpus/templates/clips/desktop/src/overlays/live-transcript.tsx +43 -14
- package/corpus/templates/clips/desktop/src/overlays/recording-pill.tsx +3 -3
- package/corpus/templates/clips/desktop/src-tauri/src/echo_guard.rs +443 -0
- package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +1 -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/src-tauri/src/whisper_speech.rs +125 -16
- 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/.agents/skills/document-editing/references/databases.md +6 -4
- package/corpus/templates/content/actions/_database-membership-lock.ts +25 -0
- package/corpus/templates/content/actions/_database-row-batch.ts +6 -2
- package/corpus/templates/content/actions/_database-source-utils.ts +289 -129
- package/corpus/templates/content/actions/_database-utils.ts +12 -3
- package/corpus/templates/content/actions/add-comment.ts +15 -1
- package/corpus/templates/content/actions/add-content-database-source-field-property.ts +39 -21
- package/corpus/templates/content/actions/attach-content-database-source.ts +4 -1
- package/corpus/templates/content/actions/change-content-database-source-role.ts +3 -5
- package/corpus/templates/content/actions/duplicate-document-property.ts +59 -46
- package/corpus/templates/content/actions/execute-builder-source-execution.ts +77 -71
- package/corpus/templates/content/actions/list-documents.ts +1 -0
- package/corpus/templates/content/actions/materialize-builder-required-fields.ts +7 -0
- package/corpus/templates/content/actions/remove-database-items.ts +183 -0
- package/corpus/templates/content/actions/set-document-property.ts +57 -39
- package/corpus/templates/content/actions/sync-local-folder-source.ts +7 -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/editor/database/DatabaseView.tsx +471 -329
- package/corpus/templates/content/app/components/editor/database/row-access.ts +45 -0
- package/corpus/templates/content/app/components/editor/database/shared.tsx +141 -38
- package/corpus/templates/content/app/components/ui/switch.tsx +1 -0
- package/corpus/templates/content/app/hooks/use-content-database.ts +2 -2
- package/corpus/templates/content/app/hooks/use-content-prefs.ts +55 -0
- package/corpus/templates/content/app/i18n-data.ts +170 -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-07-30-database-rows-can-now-be-removed-without-deleting-their-page.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/parity/eval-scenarios.ts +2 -2
- package/corpus/templates/content/parity/matrix.md +1 -1
- package/corpus/templates/content/parity/matrix.ts +4 -5
- 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/api.ts +4 -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/.generated/bridge/editor-chrome.generated.ts +26 -11
- package/corpus/templates/design/actions/take-design-screenshot.ts +7 -0
- package/corpus/templates/design/app/components/design/DesignCanvas.tsx +11 -3
- package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +4 -1
- package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +54 -17
- package/corpus/templates/design/app/components/design/design-canvas/local-runtime.ts +126 -0
- package/corpus/templates/design/app/components/templates/TemplatePreview.tsx +2 -0
- package/corpus/templates/design/app/pages/Index.tsx +3 -1
- package/corpus/templates/design/app/pages/Present.tsx +2 -1
- package/corpus/templates/design/app/routes/settings.tsx +14 -22
- package/corpus/templates/design/changelog/2026-07-30-a-broken-inline-script-in-a-generated-screen-is-now-caught-o.md +6 -0
- package/corpus/templates/design/changelog/2026-07-30-design-html-is-now-validated-with-a-spec-grade-html-parser-i.md +6 -0
- package/corpus/templates/design/changelog/2026-07-30-designs-now-load-their-tailwind-and-alpine-runtimes-from-the.md +6 -0
- package/corpus/templates/design/changelog/2026-07-30-designs-with-a-broken-alpine-expression-are-now-caught-on-sa.md +6 -0
- package/corpus/templates/design/changelog/2026-07-30-editing-a-design-no-longer-strips-its-styling-the-canvas-kep.md +6 -0
- 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/package.json +4 -0
- package/corpus/templates/design/shared/html-integrity.ts +802 -556
- 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 +2 -2
- 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/docs/content/template-content-developers.mdx +1 -1
- 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
- package/corpus/templates/content/actions/delete-database-items.ts +0 -89
|
@@ -193,6 +193,12 @@ const migrations = runMigrations(
|
|
|
193
193
|
description TEXT NOT NULL DEFAULT '',
|
|
194
194
|
thumbnail_url TEXT,
|
|
195
195
|
animated_thumbnail_url TEXT,
|
|
196
|
+
filmstrip_url TEXT,
|
|
197
|
+
filmstrip_frame_count INTEGER NOT NULL DEFAULT 0,
|
|
198
|
+
filmstrip_columns INTEGER NOT NULL DEFAULT 0,
|
|
199
|
+
filmstrip_rows INTEGER NOT NULL DEFAULT 0,
|
|
200
|
+
filmstrip_frame_width INTEGER NOT NULL DEFAULT 0,
|
|
201
|
+
filmstrip_frame_height INTEGER NOT NULL DEFAULT 0,
|
|
196
202
|
duration_ms INTEGER NOT NULL DEFAULT 0,
|
|
197
203
|
video_url TEXT,
|
|
198
204
|
video_format TEXT NOT NULL DEFAULT 'webm',
|
|
@@ -899,6 +905,16 @@ const migrations = runMigrations(
|
|
|
899
905
|
`UPDATE recordings SET upload_lease_expires_at = '${uploadLeaseExpiry()}' WHERE upload_lease_expires_at IS NULL AND status IN ('uploading', 'processing')`,
|
|
900
906
|
].join("; "),
|
|
901
907
|
},
|
|
908
|
+
{
|
|
909
|
+
version: 54,
|
|
910
|
+
name: "recording-upload-attempt-fence",
|
|
911
|
+
sql: `ALTER TABLE recordings ADD COLUMN IF NOT EXISTS upload_attempt_id TEXT`,
|
|
912
|
+
},
|
|
913
|
+
{
|
|
914
|
+
version: 55,
|
|
915
|
+
name: "recording-upload-generation-fence",
|
|
916
|
+
sql: `ALTER TABLE recordings ADD COLUMN IF NOT EXISTS upload_generation_id TEXT`,
|
|
917
|
+
},
|
|
902
918
|
],
|
|
903
919
|
{ table: "clips_migrations" },
|
|
904
920
|
);
|
|
@@ -66,6 +66,7 @@ export default defineEventHandler(async (event: H3Event) => {
|
|
|
66
66
|
ownerEmail: schema.recordings.ownerEmail,
|
|
67
67
|
orgId: schema.recordings.orgId,
|
|
68
68
|
status: schema.recordings.status,
|
|
69
|
+
uploadGenerationId: schema.recordings.uploadGenerationId,
|
|
69
70
|
})
|
|
70
71
|
.from(schema.recordings)
|
|
71
72
|
.where(eq(schema.recordings.id, recordingId))
|
|
@@ -120,6 +121,9 @@ export default defineEventHandler(async (event: H3Event) => {
|
|
|
120
121
|
const result = await finalizeRecording.run({
|
|
121
122
|
id: recordingId,
|
|
122
123
|
mediaVerificationRetryAttempt: retryAttempt ?? 1,
|
|
124
|
+
...(recording.uploadGenerationId
|
|
125
|
+
? { uploadGenerationId: recording.uploadGenerationId }
|
|
126
|
+
: {}),
|
|
123
127
|
});
|
|
124
128
|
return { ok: true, kind, result };
|
|
125
129
|
}
|
|
@@ -6,14 +6,13 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import {
|
|
9
|
-
|
|
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() });
|
|
@@ -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);
|