@agent-native/core 0.108.0 → 0.109.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/corpus/README.md +2 -2
- package/corpus/core/CHANGELOG.md +25 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/a2a/client.ts +162 -17
- package/corpus/core/src/a2a/correlation.ts +50 -0
- package/corpus/core/src/a2a/handlers.ts +200 -14
- package/corpus/core/src/a2a/index.ts +7 -1
- package/corpus/core/src/a2a/invoke.ts +77 -1
- package/corpus/core/src/a2a/server.ts +13 -1
- package/corpus/core/src/a2a/task-store.ts +146 -6
- package/corpus/core/src/a2a/types.ts +32 -0
- package/corpus/core/src/action.ts +5 -2
- package/corpus/core/src/agent/production-agent.ts +33 -4
- package/corpus/core/src/client/AssistantChat.tsx +648 -603
- package/corpus/core/src/client/MultiTabAssistantChat.tsx +31 -4
- package/corpus/core/src/client/agent-chat-adapter.ts +23 -37
- package/corpus/core/src/client/agent-sidebar-state.ts +4 -0
- package/corpus/core/src/client/analytics.ts +84 -0
- package/corpus/core/src/client/chat/message-components.tsx +94 -22
- package/corpus/core/src/client/chat/tool-call-display.tsx +170 -47
- package/corpus/core/src/client/chat/use-agent-chat-lifecycle-tracking.ts +74 -0
- package/corpus/core/src/client/chat-thread-url.ts +11 -0
- package/corpus/core/src/client/error-capture.ts +12 -0
- package/corpus/core/src/client/extensions/ExtensionSlot.tsx +37 -1
- package/corpus/core/src/client/index.ts +2 -0
- package/corpus/core/src/client/route-chunk-recovery.ts +20 -8
- package/corpus/core/src/client/session-replay.ts +31 -0
- package/corpus/core/src/client/use-run-stuck-detection.ts +27 -1
- package/corpus/core/src/db/client.ts +55 -0
- package/corpus/core/src/extensions/actions.ts +2 -14
- package/corpus/core/src/extensions/store.ts +1 -1
- package/corpus/core/src/localization/default-messages.ts +3 -0
- package/corpus/core/src/mcp/build-server.ts +2 -2
- package/corpus/core/src/observability/traces.ts +172 -19
- package/corpus/core/src/progress/actions.ts +4 -2
- package/corpus/core/src/provider-api/index.ts +205 -105
- package/corpus/core/src/scripts/call-agent.ts +190 -19
- package/corpus/core/src/server/agent-chat/action-filters-a2a.ts +112 -25
- package/corpus/core/src/server/agent-chat-plugin.ts +136 -4
- package/corpus/core/src/styles/agent-conversation.css +7 -0
- package/corpus/core/src/templates/default/app/i18n/ar-SA.ts +20 -0
- package/corpus/core/src/templates/default/app/i18n/de-DE.ts +20 -0
- package/corpus/core/src/templates/default/app/i18n/en-US.ts +20 -0
- package/corpus/core/src/templates/default/app/i18n/es-ES.ts +20 -0
- package/corpus/core/src/templates/default/app/i18n/fr-FR.ts +21 -0
- package/corpus/core/src/templates/default/app/i18n/hi-IN.ts +19 -0
- package/corpus/core/src/templates/default/app/i18n/ja-JP.ts +20 -0
- package/corpus/core/src/templates/default/app/i18n/ko-KR.ts +19 -0
- package/corpus/core/src/templates/default/app/i18n/pt-BR.ts +20 -0
- package/corpus/core/src/templates/default/app/i18n/zh-CN.ts +19 -0
- package/corpus/core/src/templates/default/app/i18n/zh-TW.ts +19 -0
- package/corpus/core/src/templates/workspace-core/.agents/skills/a2a-protocol/SKILL.md +49 -0
- package/corpus/core/src/templates/workspace-core/.agents/skills/composable-mini-apps/SKILL.md +5 -0
- package/corpus/core/src/templates/workspace-core/.agents/skills/observability/SKILL.md +7 -1
- package/corpus/core/src/templates/workspace-core/.agents/skills/performance/SKILL.md +13 -0
- package/corpus/core/src/templates/workspace-core/.agents/skills/tracking/SKILL.md +1 -1
- package/corpus/templates/analytics/.agents/skills/dashboard-management/SKILL.md +29 -10
- package/corpus/templates/analytics/.agents/skills/gong/SKILL.md +54 -23
- package/corpus/templates/analytics/.builder/skills/apollo/SKILL.md +19 -41
- package/corpus/templates/analytics/.builder/skills/commonroom/SKILL.md +20 -35
- package/corpus/templates/analytics/.builder/skills/github/SKILL.md +23 -67
- package/corpus/templates/analytics/.builder/skills/pylon/SKILL.md +25 -43
- package/corpus/templates/analytics/AGENTS.md +22 -13
- package/corpus/templates/analytics/actions/account-deep-dive.ts +1 -0
- package/corpus/templates/analytics/actions/configure-gong-native-insights.ts +29 -0
- package/corpus/templates/analytics/actions/content-calendar-schema.ts +8 -4
- package/corpus/templates/analytics/actions/content-calendar.ts +12 -4
- package/corpus/templates/analytics/actions/dashboard-mutation-api.ts +2 -1
- package/corpus/templates/analytics/actions/gong-calls.ts +113 -22
- package/corpus/templates/analytics/actions/gong-native-insights.ts +166 -0
- package/corpus/templates/analytics/actions/hubspot-deals.ts +190 -46
- package/corpus/templates/analytics/actions/pylon-issues.ts +123 -0
- package/corpus/templates/analytics/actions/query-agent-native-analytics.ts +1 -1
- package/corpus/templates/analytics/actions/slack-messages.ts +179 -27
- package/corpus/templates/analytics/app/components/dashboard/SqlChart.tsx +48 -152
- package/corpus/templates/analytics/app/components/ui/chart.tsx +1 -1
- package/corpus/templates/analytics/app/hooks/use-chart-tooltip-flip.ts +21 -6
- package/corpus/templates/analytics/app/i18n/zh-TW.ts +13 -0
- package/corpus/templates/analytics/app/i18n-data.ts +134 -0
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/PanelEditorDialog.tsx +181 -30
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/SqlChartCard.tsx +6 -0
- package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/types.ts +4 -3
- package/corpus/templates/analytics/changelog/2026-07-17-analytics-now-includes-a-default-alert-for-spikes-in-stuck-agent-chats.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-17-dashboard-editors-can-add-shared-extensions-or-opt-into-per-.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-17-dashboard-extensions-show-a-loading-skeleton.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-17-errors-now-group-repeat-failures-consistently-and-keep-priva.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-17-gong-transcript-reviews-now-batch-bounded-call-windows-for-f.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-17-hubspot-deal-filters-now-run-in-hubspot-before-paging-result.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-17-notion-content-calendars-now-discover-the-matching-database-.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-17-pylon-dashboard-reads-now-use-the-paginated-issue-search-api.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-17-slack-reads-no-longer-join-channels-and-report-pagination-coverage.md +6 -0
- package/corpus/templates/analytics/docs/schemas/first-party-analytics.md +23 -14
- package/corpus/templates/analytics/docs/schemas/tracked-events.md +5 -1
- package/corpus/templates/analytics/server/handlers/notion.ts +14 -3
- package/corpus/templates/analytics/server/handlers/slack.ts +182 -29
- package/corpus/templates/analytics/server/jobs/analytics-alerts.ts +3 -3
- package/corpus/templates/analytics/server/lib/agent-chat-plan-mode.ts +2 -6
- package/corpus/templates/analytics/server/lib/analytics-alerts.ts +209 -87
- package/corpus/templates/analytics/server/lib/analytics-connector-catalog.ts +6 -2
- package/corpus/templates/analytics/server/lib/error-capture.ts +75 -8
- package/corpus/templates/analytics/server/lib/gong-native-policy.ts +72 -0
- package/corpus/templates/analytics/server/lib/gong.ts +54 -66
- package/corpus/templates/analytics/server/lib/hubspot.ts +40 -3
- package/corpus/templates/analytics/server/lib/notion.ts +83 -8
- package/corpus/templates/analytics/server/lib/real-data-actions.ts +4 -0
- package/corpus/templates/analytics/server/lib/slack.ts +242 -90
- package/corpus/templates/analytics/server/plugins/agent-chat.ts +3 -3
- package/corpus/templates/analytics/server/routes/api/pylon/accounts.get.ts +42 -1
- package/corpus/templates/analytics/server/routes/api/pylon/issues.get.ts +71 -1
- package/corpus/templates/analytics/server/routes/api/test-connection.post.ts +44 -4
- package/corpus/templates/clips/actions/finalize-recording.ts +541 -62
- package/corpus/templates/clips/app/components/library/bulk-action-toolbar.tsx +19 -1
- package/corpus/templates/clips/app/components/library/create-folder-dialog.tsx +123 -0
- package/corpus/templates/clips/app/components/library/library-grid.tsx +42 -4
- package/corpus/templates/clips/app/components/library/recording-card.tsx +30 -0
- package/corpus/templates/clips/app/components/player/playback-comment-overlay.tsx +17 -2
- package/corpus/templates/clips/app/components/player/video-player.tsx +14 -2
- package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +2 -2
- package/corpus/templates/clips/app/entry.client.tsx +6 -1
- package/corpus/templates/clips/app/i18n/ar-SA.ts +12 -1
- package/corpus/templates/clips/app/i18n/de-DE.ts +12 -1
- package/corpus/templates/clips/app/i18n/en-US.ts +12 -1
- package/corpus/templates/clips/app/i18n/es-ES.ts +11 -1
- package/corpus/templates/clips/app/i18n/fr-FR.ts +11 -1
- package/corpus/templates/clips/app/i18n/hi-IN.ts +10 -1
- package/corpus/templates/clips/app/i18n/ja-JP.ts +12 -1
- package/corpus/templates/clips/app/i18n/ko-KR.ts +11 -1
- package/corpus/templates/clips/app/i18n/pt-BR.ts +11 -1
- package/corpus/templates/clips/app/i18n/zh-CN.ts +11 -1
- package/corpus/templates/clips/app/i18n/zh-TW.ts +11 -1
- package/corpus/templates/clips/app/routes/_app.settings._index.tsx +72 -1
- package/corpus/templates/clips/app/routes/r.$recordingId.tsx +1 -1
- package/corpus/templates/clips/app/routes/record.tsx +3 -3
- package/corpus/templates/clips/app/routes/share.$shareId.tsx +6 -3
- package/corpus/templates/clips/changelog/2026-07-17-choose-which-workspace-receives-new-clips-recordings-and-des.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-17-clips-now-retries-storage-verification-in-the-background-ins.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-17-create-folders-directly-from-the-move-menu.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-17-failed-desktop-uploads-now-offer-a-direct-way-to-reopen-clip.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-17-fixed-broken-thumbnail-icons-appearing-over-playable-clip-em.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-17-playback-comments-stay-readable-at-faster-playback-speeds.md +6 -0
- package/corpus/templates/clips/chrome-extension/src/finalize-recovery.ts +3 -0
- package/corpus/templates/clips/chrome-extension/src/offscreen.ts +3 -3
- package/corpus/templates/clips/desktop/src/app.tsx +19 -7
- package/corpus/templates/clips/desktop/src/lib/recorder.ts +111 -13
- package/corpus/templates/clips/desktop/src/lib/upload-verification.ts +16 -7
- package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +2 -4
- package/corpus/templates/clips/desktop/src-tauri/src/native_screen/live_upload.rs +32 -15
- package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +45 -18
- package/corpus/templates/clips/server/jobs/media-verification.ts +130 -0
- package/corpus/templates/clips/server/lib/builder-media-compression.ts +15 -0
- package/corpus/templates/clips/server/lib/media-verification-state.ts +38 -0
- package/corpus/templates/clips/server/lib/player-thumbnail-url.ts +31 -0
- package/corpus/templates/clips/server/lib/post-finalize-dispatch.ts +10 -3
- package/corpus/templates/clips/server/plugins/auth.ts +5 -5
- package/corpus/templates/clips/server/plugins/calendar-jobs.ts +2 -0
- package/corpus/templates/clips/server/routes/api/_agent-native-background/post-finalize-worker.post.ts +12 -2
- package/corpus/templates/clips/server/routes/api/public-recording.get.ts +6 -1
- package/corpus/templates/clips/server/routes/api/thumbnail/[recordingId].get.ts +340 -0
- package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/abort.post.ts +24 -7
- package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/chunk.post.ts +91 -7
- package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/status.get.ts +14 -0
- package/corpus/templates/clips/shared/finalize-recovery.ts +61 -2
- package/corpus/templates/clips/shared/share-meta.ts +1 -1
- package/corpus/templates/content/app/entry.client.tsx +6 -1
- package/corpus/templates/dispatch/AGENTS.md +5 -0
- package/corpus/templates/dispatch/changelog/2026-07-17-apps-page-links-to-other-available-apps.md +6 -0
- package/corpus/templates/dispatch/changelog/2026-07-17-curated-template-remixes.md +6 -0
- package/corpus/templates/mail/app/entry.client.tsx +6 -1
- package/corpus/templates/slides/.agents/skills/slide-images/SKILL.md +11 -7
- package/corpus/templates/slides/.env.example +0 -1
- package/corpus/templates/slides/AGENTS.md +4 -0
- package/corpus/templates/slides/DEVELOPING.md +2 -2
- package/corpus/templates/slides/actions/index-design-system-with-builder.ts +1 -1
- package/corpus/templates/slides/actions/logo-config.ts +6 -7
- package/corpus/templates/slides/actions/search-images.ts +11 -39
- package/corpus/templates/slides/actions/search-logos.ts +5 -52
- package/corpus/templates/slides/app/components/editor/ImageSearchPanel.tsx +1 -1
- package/corpus/templates/slides/app/components/editor/LogoSearchPanel.tsx +16 -26
- package/corpus/templates/slides/app/entry.client.tsx +6 -1
- package/corpus/templates/slides/server/lib/media-search-providers.ts +216 -0
- package/corpus/templates/slides/server/register-secrets.ts +30 -0
- package/dist/a2a/client.d.ts +47 -1
- package/dist/a2a/client.d.ts.map +1 -1
- package/dist/a2a/client.js +94 -12
- package/dist/a2a/client.js.map +1 -1
- package/dist/a2a/correlation.d.ts +10 -0
- package/dist/a2a/correlation.d.ts.map +1 -0
- package/dist/a2a/correlation.js +40 -0
- package/dist/a2a/correlation.js.map +1 -0
- package/dist/a2a/handlers.d.ts.map +1 -1
- package/dist/a2a/handlers.js +147 -15
- package/dist/a2a/handlers.js.map +1 -1
- package/dist/a2a/index.d.ts +4 -4
- package/dist/a2a/index.d.ts.map +1 -1
- package/dist/a2a/index.js +2 -2
- package/dist/a2a/index.js.map +1 -1
- package/dist/a2a/invoke.d.ts +29 -2
- package/dist/a2a/invoke.d.ts.map +1 -1
- package/dist/a2a/invoke.js +33 -1
- package/dist/a2a/invoke.js.map +1 -1
- package/dist/a2a/server.d.ts.map +1 -1
- package/dist/a2a/server.js +14 -1
- package/dist/a2a/server.js.map +1 -1
- package/dist/a2a/task-store.d.ts +12 -1
- package/dist/a2a/task-store.d.ts.map +1 -1
- package/dist/a2a/task-store.js +96 -6
- package/dist/a2a/task-store.js.map +1 -1
- package/dist/a2a/types.d.ts +27 -0
- package/dist/a2a/types.d.ts.map +1 -1
- package/dist/a2a/types.js.map +1 -1
- package/dist/action.d.ts +5 -2
- package/dist/action.d.ts.map +1 -1
- package/dist/action.js.map +1 -1
- package/dist/agent/production-agent.d.ts +14 -0
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +19 -3
- package/dist/agent/production-agent.js.map +1 -1
- package/dist/client/AssistantChat.d.ts +14 -1
- package/dist/client/AssistantChat.d.ts.map +1 -1
- package/dist/client/AssistantChat.js +158 -136
- package/dist/client/AssistantChat.js.map +1 -1
- package/dist/client/MultiTabAssistantChat.d.ts.map +1 -1
- package/dist/client/MultiTabAssistantChat.js +27 -4
- package/dist/client/MultiTabAssistantChat.js.map +1 -1
- package/dist/client/agent-chat-adapter.d.ts.map +1 -1
- package/dist/client/agent-chat-adapter.js +21 -39
- package/dist/client/agent-chat-adapter.js.map +1 -1
- package/dist/client/agent-sidebar-state.d.ts +1 -0
- package/dist/client/agent-sidebar-state.d.ts.map +1 -1
- package/dist/client/agent-sidebar-state.js +4 -0
- package/dist/client/agent-sidebar-state.js.map +1 -1
- package/dist/client/analytics.d.ts +14 -0
- package/dist/client/analytics.d.ts.map +1 -1
- package/dist/client/analytics.js +67 -0
- package/dist/client/analytics.js.map +1 -1
- package/dist/client/chat/message-components.d.ts +8 -0
- package/dist/client/chat/message-components.d.ts.map +1 -1
- package/dist/client/chat/message-components.js +62 -19
- package/dist/client/chat/message-components.js.map +1 -1
- package/dist/client/chat/tool-call-display.d.ts +6 -0
- package/dist/client/chat/tool-call-display.d.ts.map +1 -1
- package/dist/client/chat/tool-call-display.js +83 -14
- package/dist/client/chat/tool-call-display.js.map +1 -1
- package/dist/client/chat/use-agent-chat-lifecycle-tracking.d.ts +8 -0
- package/dist/client/chat/use-agent-chat-lifecycle-tracking.d.ts.map +1 -0
- package/dist/client/chat/use-agent-chat-lifecycle-tracking.js +51 -0
- package/dist/client/chat/use-agent-chat-lifecycle-tracking.js.map +1 -0
- package/dist/client/chat-thread-url.d.ts +2 -0
- package/dist/client/chat-thread-url.d.ts.map +1 -0
- package/dist/client/chat-thread-url.js +12 -0
- package/dist/client/chat-thread-url.js.map +1 -0
- package/dist/client/error-capture.d.ts.map +1 -1
- package/dist/client/error-capture.js +7 -0
- package/dist/client/error-capture.js.map +1 -1
- package/dist/client/extensions/ExtensionSlot.d.ts +3 -1
- package/dist/client/extensions/ExtensionSlot.d.ts.map +1 -1
- package/dist/client/extensions/ExtensionSlot.js +28 -3
- package/dist/client/extensions/ExtensionSlot.js.map +1 -1
- package/dist/client/index.d.ts +1 -1
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +1 -1
- package/dist/client/index.js.map +1 -1
- package/dist/client/route-chunk-recovery.d.ts.map +1 -1
- package/dist/client/route-chunk-recovery.js +14 -9
- package/dist/client/route-chunk-recovery.js.map +1 -1
- package/dist/client/session-replay.d.ts +15 -0
- package/dist/client/session-replay.d.ts.map +1 -1
- package/dist/client/session-replay.js +20 -0
- package/dist/client/session-replay.js.map +1 -1
- package/dist/client/use-run-stuck-detection.d.ts.map +1 -1
- package/dist/client/use-run-stuck-detection.js +25 -1
- package/dist/client/use-run-stuck-detection.js.map +1 -1
- package/dist/db/client.d.ts +7 -0
- package/dist/db/client.d.ts.map +1 -1
- package/dist/db/client.js +33 -0
- package/dist/db/client.js.map +1 -1
- package/dist/extensions/actions.d.ts.map +1 -1
- package/dist/extensions/actions.js +2 -10
- package/dist/extensions/actions.js.map +1 -1
- package/dist/extensions/store.js +1 -1
- package/dist/extensions/store.js.map +1 -1
- package/dist/localization/default-messages.d.ts +3 -0
- package/dist/localization/default-messages.d.ts.map +1 -1
- package/dist/localization/default-messages.js +3 -0
- package/dist/localization/default-messages.js.map +1 -1
- package/dist/mcp/build-server.d.ts +2 -0
- package/dist/mcp/build-server.d.ts.map +1 -1
- package/dist/mcp/build-server.js +2 -2
- package/dist/mcp/build-server.js.map +1 -1
- package/dist/notifications/routes.d.ts +2 -2
- package/dist/observability/routes.d.ts +5 -5
- package/dist/observability/traces.d.ts +9 -0
- package/dist/observability/traces.d.ts.map +1 -1
- package/dist/observability/traces.js +137 -19
- package/dist/observability/traces.js.map +1 -1
- package/dist/progress/actions.d.ts.map +1 -1
- package/dist/progress/actions.js +3 -2
- package/dist/progress/actions.js.map +1 -1
- package/dist/progress/routes.d.ts +1 -1
- package/dist/provider-api/corpus-jobs.d.ts +2 -2
- package/dist/provider-api/index.d.ts.map +1 -1
- package/dist/provider-api/index.js +91 -8
- package/dist/provider-api/index.js.map +1 -1
- package/dist/scripts/call-agent.d.ts.map +1 -1
- package/dist/scripts/call-agent.js +140 -17
- package/dist/scripts/call-agent.js.map +1 -1
- package/dist/secrets/routes.d.ts +9 -9
- package/dist/server/agent-chat/action-filters-a2a.d.ts +24 -2
- package/dist/server/agent-chat/action-filters-a2a.d.ts.map +1 -1
- package/dist/server/agent-chat/action-filters-a2a.js +62 -7
- package/dist/server/agent-chat/action-filters-a2a.js.map +1 -1
- package/dist/server/agent-chat-plugin.d.ts.map +1 -1
- package/dist/server/agent-chat-plugin.js +118 -5
- package/dist/server/agent-chat-plugin.js.map +1 -1
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/dist/styles/agent-conversation.css +7 -0
- package/dist/templates/default/app/i18n/ar-SA.ts +20 -0
- package/dist/templates/default/app/i18n/de-DE.ts +20 -0
- package/dist/templates/default/app/i18n/en-US.ts +20 -0
- package/dist/templates/default/app/i18n/es-ES.ts +20 -0
- package/dist/templates/default/app/i18n/fr-FR.ts +21 -0
- package/dist/templates/default/app/i18n/hi-IN.ts +19 -0
- package/dist/templates/default/app/i18n/ja-JP.ts +20 -0
- package/dist/templates/default/app/i18n/ko-KR.ts +19 -0
- package/dist/templates/default/app/i18n/pt-BR.ts +20 -0
- package/dist/templates/default/app/i18n/zh-CN.ts +19 -0
- package/dist/templates/default/app/i18n/zh-TW.ts +19 -0
- package/dist/templates/workspace-core/.agents/skills/a2a-protocol/SKILL.md +49 -0
- package/dist/templates/workspace-core/.agents/skills/composable-mini-apps/SKILL.md +5 -0
- package/dist/templates/workspace-core/.agents/skills/observability/SKILL.md +7 -1
- package/dist/templates/workspace-core/.agents/skills/performance/SKILL.md +13 -0
- package/dist/templates/workspace-core/.agents/skills/tracking/SKILL.md +1 -1
- package/package.json +1 -1
- package/src/templates/default/app/i18n/ar-SA.ts +20 -0
- package/src/templates/default/app/i18n/de-DE.ts +20 -0
- package/src/templates/default/app/i18n/en-US.ts +20 -0
- package/src/templates/default/app/i18n/es-ES.ts +20 -0
- package/src/templates/default/app/i18n/fr-FR.ts +21 -0
- package/src/templates/default/app/i18n/hi-IN.ts +19 -0
- package/src/templates/default/app/i18n/ja-JP.ts +20 -0
- package/src/templates/default/app/i18n/ko-KR.ts +19 -0
- package/src/templates/default/app/i18n/pt-BR.ts +20 -0
- package/src/templates/default/app/i18n/zh-CN.ts +19 -0
- package/src/templates/default/app/i18n/zh-TW.ts +19 -0
- package/src/templates/workspace-core/.agents/skills/a2a-protocol/SKILL.md +49 -0
- package/src/templates/workspace-core/.agents/skills/composable-mini-apps/SKILL.md +5 -0
- package/src/templates/workspace-core/.agents/skills/observability/SKILL.md +7 -1
- package/src/templates/workspace-core/.agents/skills/performance/SKILL.md +13 -0
- package/src/templates/workspace-core/.agents/skills/tracking/SKILL.md +1 -1
- package/corpus/templates/analytics/server/handlers/github.ts +0 -200
- package/corpus/templates/analytics/server/handlers/pylon.ts +0 -43
- package/corpus/templates/analytics/server/lib/apollo.ts +0 -167
- package/corpus/templates/analytics/server/lib/commonroom.ts +0 -149
- package/corpus/templates/analytics/server/lib/github.ts +0 -660
- package/corpus/templates/analytics/server/lib/mixpanel.ts +0 -156
- package/corpus/templates/analytics/server/lib/posthog.ts +0 -194
- package/corpus/templates/analytics/server/lib/pylon.ts +0 -109
- package/corpus/templates/analytics/server/routes/api/github/graphql.post.ts +0 -1
- package/corpus/templates/analytics/server/routes/api/github/issue.get.ts +0 -1
- package/corpus/templates/analytics/server/routes/api/github/org-prs.get.ts +0 -1
- package/corpus/templates/analytics/server/routes/api/github/pr.get.ts +0 -1
- package/corpus/templates/analytics/server/routes/api/github/prs.get.ts +0 -1
- package/corpus/templates/analytics/server/routes/api/github/search.get.ts +0 -1
- package/corpus/templates/slides/actions/fetch-logos.ts +0 -90
- package/corpus/templates/slides/actions/image-search.ts +0 -64
- package/corpus/templates/slides/actions/import-github.ts +0 -182
- package/corpus/templates/slides/actions/logo-lookup.ts +0 -113
|
@@ -10,16 +10,17 @@
|
|
|
10
10
|
|
|
11
11
|
import { defineAction } from "@agent-native/core";
|
|
12
12
|
import {
|
|
13
|
+
compareAndSetAppState,
|
|
14
|
+
deleteAppState,
|
|
13
15
|
readAppState,
|
|
14
16
|
writeAppState,
|
|
15
|
-
deleteAppState,
|
|
16
17
|
} from "@agent-native/core/application-state";
|
|
17
18
|
import { emit } from "@agent-native/core/event-bus";
|
|
18
19
|
import { uploadFile } from "@agent-native/core/file-upload";
|
|
19
20
|
import { captureRouteError } from "@agent-native/core/server";
|
|
20
21
|
import { isStoredButUnservableFinalizeError } from "@shared/finalize-recovery.js";
|
|
21
22
|
import { MAX_UPLOAD_BYTES as MAX_RECORDING_UPLOAD_BYTES } from "@shared/upload-limits.js";
|
|
22
|
-
import { and, eq, isNull
|
|
23
|
+
import { and, eq, isNull } from "drizzle-orm";
|
|
23
24
|
import { z } from "zod";
|
|
24
25
|
|
|
25
26
|
import { getDb, schema } from "../server/db/index.js";
|
|
@@ -29,6 +30,10 @@ import {
|
|
|
29
30
|
applyFaststart,
|
|
30
31
|
hasPlayableMp4Metadata,
|
|
31
32
|
} from "../server/lib/faststart.js";
|
|
33
|
+
import {
|
|
34
|
+
mediaVerificationStateKey,
|
|
35
|
+
parseMediaVerificationMarker,
|
|
36
|
+
} from "../server/lib/media-verification-state.js";
|
|
32
37
|
import { dispatchPostFinalizeJob } from "../server/lib/post-finalize-dispatch.js";
|
|
33
38
|
import {
|
|
34
39
|
listRecordingChunkKeys,
|
|
@@ -97,6 +102,8 @@ const RECORDING_TOO_LARGE_REASON =
|
|
|
97
102
|
const MEDIA_SERVE_VERIFICATION_TIMEOUT_MS = 8_000;
|
|
98
103
|
const MEDIA_SERVE_VERIFICATION_ATTEMPTS = 3;
|
|
99
104
|
const MEDIA_SERVE_VERIFICATION_BACKOFF_MS = 350;
|
|
105
|
+
const MEDIA_VERIFICATION_MAX_DURABLE_ATTEMPTS = 10;
|
|
106
|
+
const MEDIA_VERIFICATION_INITIAL_RETRY_DELAY_MS = 5_000;
|
|
100
107
|
|
|
101
108
|
function stateNumber(
|
|
102
109
|
value: Record<string, unknown> | null | undefined,
|
|
@@ -115,6 +122,14 @@ function stateBoolean(
|
|
|
115
122
|
return typeof raw === "boolean" ? raw : undefined;
|
|
116
123
|
}
|
|
117
124
|
|
|
125
|
+
function stateString(
|
|
126
|
+
value: Record<string, unknown> | null | undefined,
|
|
127
|
+
key: string,
|
|
128
|
+
): string | undefined {
|
|
129
|
+
const raw = value?.[key];
|
|
130
|
+
return typeof raw === "string" && raw.trim() ? raw : undefined;
|
|
131
|
+
}
|
|
132
|
+
|
|
118
133
|
const cliBoolean = z.preprocess((value) => {
|
|
119
134
|
if (value === "true") return true;
|
|
120
135
|
if (value === "false") return false;
|
|
@@ -166,11 +181,8 @@ function servedMediaSizeBytes(response: Response): number | null {
|
|
|
166
181
|
return null;
|
|
167
182
|
}
|
|
168
183
|
|
|
169
|
-
async function verifyServedMediaUrl(
|
|
170
|
-
videoUrl
|
|
171
|
-
expectedBytes: number,
|
|
172
|
-
): Promise<void> {
|
|
173
|
-
if (!shouldVerifyServedMediaUrl(videoUrl)) return;
|
|
184
|
+
async function verifyServedMediaUrl(videoUrl: string): Promise<number | null> {
|
|
185
|
+
if (!shouldVerifyServedMediaUrl(videoUrl)) return null;
|
|
174
186
|
|
|
175
187
|
let lastFailure = "media URL did not serve readable bytes";
|
|
176
188
|
for (
|
|
@@ -194,10 +206,12 @@ async function verifyServedMediaUrl(
|
|
|
194
206
|
const servedBytes = servedMediaSizeBytes(response);
|
|
195
207
|
if (servedBytes === null) {
|
|
196
208
|
lastFailure = "Stored media byte count could not be verified";
|
|
197
|
-
} else if (servedBytes !== expectedBytes) {
|
|
198
|
-
lastFailure = `Stored media byte count mismatch (${servedBytes} of ${expectedBytes} bytes)`;
|
|
199
209
|
} else if (await responseHasReadableMediaBytes(response)) {
|
|
200
|
-
|
|
210
|
+
// Builder stable video URLs intentionally replace the source object
|
|
211
|
+
// with a smaller compressed generation at the same URL. A positive,
|
|
212
|
+
// readable object is the invariant here; source-byte equality is
|
|
213
|
+
// verified separately by the upload receipt/local backup contract.
|
|
214
|
+
return servedBytes;
|
|
201
215
|
} else {
|
|
202
216
|
lastFailure = "media URL did not serve readable bytes";
|
|
203
217
|
}
|
|
@@ -219,6 +233,13 @@ async function verifyServedMediaUrl(
|
|
|
219
233
|
throw new Error(`Upload was stored-but-unservable: ${lastFailure}`);
|
|
220
234
|
}
|
|
221
235
|
|
|
236
|
+
function mediaVerificationRetryDelayMs(attempt: number): number {
|
|
237
|
+
return Math.min(
|
|
238
|
+
30_000,
|
|
239
|
+
MEDIA_VERIFICATION_INITIAL_RETRY_DELAY_MS * 2 ** Math.max(0, attempt - 1),
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
|
|
222
243
|
function queueBackgroundBuilderCompression(args: {
|
|
223
244
|
recordingId: string;
|
|
224
245
|
ownerEmail: string;
|
|
@@ -241,11 +262,11 @@ async function failStoredButUnservableRecording(params: {
|
|
|
241
262
|
id: string;
|
|
242
263
|
ownerEmail: string;
|
|
243
264
|
failureReason: string;
|
|
244
|
-
}): Promise<
|
|
265
|
+
}): Promise<boolean> {
|
|
245
266
|
const { id, ownerEmail, failureReason } = params;
|
|
246
267
|
const now = new Date().toISOString();
|
|
247
268
|
const db = getDb();
|
|
248
|
-
await db
|
|
269
|
+
const failed = await db
|
|
249
270
|
.update(schema.recordings)
|
|
250
271
|
.set({
|
|
251
272
|
status: "failed",
|
|
@@ -256,8 +277,11 @@ async function failStoredButUnservableRecording(params: {
|
|
|
256
277
|
and(
|
|
257
278
|
eq(schema.recordings.id, id),
|
|
258
279
|
ownerEmailMatches(schema.recordings.ownerEmail, ownerEmail),
|
|
280
|
+
eq(schema.recordings.status, "processing"),
|
|
259
281
|
),
|
|
260
|
-
)
|
|
282
|
+
)
|
|
283
|
+
.returning({ id: schema.recordings.id });
|
|
284
|
+
if (failed.length !== 1) return false;
|
|
261
285
|
const uploadStateRaw = await readAppState(`recording-upload-${id}`).catch(
|
|
262
286
|
() => null,
|
|
263
287
|
);
|
|
@@ -272,7 +296,223 @@ async function failStoredButUnservableRecording(params: {
|
|
|
272
296
|
failureReason,
|
|
273
297
|
updatedAt: now,
|
|
274
298
|
});
|
|
299
|
+
await deleteAppState(mediaVerificationStateKey(id)).catch((err) => {
|
|
300
|
+
console.warn("[finalize] failed to clear media verification marker", {
|
|
301
|
+
id,
|
|
302
|
+
error: err instanceof Error ? err.message : String(err),
|
|
303
|
+
});
|
|
304
|
+
});
|
|
275
305
|
await writeAppState("refresh-signal", { ts: Date.now() });
|
|
306
|
+
return true;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
type PendingMediaVerification = {
|
|
310
|
+
videoUrl: string;
|
|
311
|
+
videoSizeBytes: number;
|
|
312
|
+
sourceSizeBytes: number;
|
|
313
|
+
videoFormat: "webm" | "mp4";
|
|
314
|
+
finalDurationMs: number;
|
|
315
|
+
finalWidth: number;
|
|
316
|
+
finalHeight: number;
|
|
317
|
+
finalHasAudio: boolean;
|
|
318
|
+
finalHasCamera: boolean;
|
|
319
|
+
seekableApplied: boolean;
|
|
320
|
+
mimeType: string;
|
|
321
|
+
providerId?: string;
|
|
322
|
+
assetDbId?: string;
|
|
323
|
+
locallyTranscoded: boolean;
|
|
324
|
+
};
|
|
325
|
+
|
|
326
|
+
function pendingMediaVerificationFromState(
|
|
327
|
+
state: Record<string, unknown> | null,
|
|
328
|
+
): PendingMediaVerification | null {
|
|
329
|
+
if (state?.pendingMediaVerification !== true) return null;
|
|
330
|
+
const videoUrl = stateString(state, "videoUrl");
|
|
331
|
+
const videoSizeBytes = stateNumber(state, "videoSizeBytes");
|
|
332
|
+
const sourceSizeBytes = stateNumber(state, "sourceSizeBytes");
|
|
333
|
+
const videoFormat = stateString(state, "videoFormat");
|
|
334
|
+
if (
|
|
335
|
+
!videoUrl ||
|
|
336
|
+
!videoSizeBytes ||
|
|
337
|
+
!sourceSizeBytes ||
|
|
338
|
+
(videoFormat !== "webm" && videoFormat !== "mp4")
|
|
339
|
+
) {
|
|
340
|
+
return null;
|
|
341
|
+
}
|
|
342
|
+
return {
|
|
343
|
+
videoUrl,
|
|
344
|
+
videoSizeBytes,
|
|
345
|
+
sourceSizeBytes,
|
|
346
|
+
videoFormat,
|
|
347
|
+
finalDurationMs: stateNumber(state, "durationMs") ?? 0,
|
|
348
|
+
finalWidth: stateNumber(state, "width") ?? 0,
|
|
349
|
+
finalHeight: stateNumber(state, "height") ?? 0,
|
|
350
|
+
finalHasAudio: stateBoolean(state, "hasAudio") ?? true,
|
|
351
|
+
finalHasCamera: stateBoolean(state, "hasCamera") ?? false,
|
|
352
|
+
seekableApplied: stateBoolean(state, "seekableApplied") ?? false,
|
|
353
|
+
mimeType: stateString(state, "mimeType") ?? `video/${videoFormat}`,
|
|
354
|
+
providerId: stateString(state, "providerId"),
|
|
355
|
+
assetDbId: stateString(state, "assetDbId"),
|
|
356
|
+
locallyTranscoded: stateBoolean(state, "locallyTranscoded") ?? false,
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
async function persistPendingMediaVerification(params: {
|
|
361
|
+
id: string;
|
|
362
|
+
ownerEmail: string;
|
|
363
|
+
media: PendingMediaVerification;
|
|
364
|
+
failureReason: string;
|
|
365
|
+
retryAttempt?: number;
|
|
366
|
+
}): Promise<boolean> {
|
|
367
|
+
const { id, ownerEmail, media, failureReason } = params;
|
|
368
|
+
const now = new Date().toISOString();
|
|
369
|
+
const retryAttempt = Math.max(0, params.retryAttempt ?? 0);
|
|
370
|
+
const nextRetryAttempt = Math.min(
|
|
371
|
+
MEDIA_VERIFICATION_MAX_DURABLE_ATTEMPTS,
|
|
372
|
+
retryAttempt + 1,
|
|
373
|
+
);
|
|
374
|
+
const nextAttemptAt = new Date(
|
|
375
|
+
Date.now() + mediaVerificationRetryDelayMs(nextRetryAttempt),
|
|
376
|
+
).toISOString();
|
|
377
|
+
const db = getDb();
|
|
378
|
+
const persisted = await db
|
|
379
|
+
.update(schema.recordings)
|
|
380
|
+
.set({
|
|
381
|
+
status: "processing",
|
|
382
|
+
videoUrl: media.videoUrl,
|
|
383
|
+
videoFormat: media.videoFormat,
|
|
384
|
+
videoSizeBytes: media.videoSizeBytes,
|
|
385
|
+
durationMs: media.finalDurationMs,
|
|
386
|
+
width: media.finalWidth,
|
|
387
|
+
height: media.finalHeight,
|
|
388
|
+
hasAudio: media.finalHasAudio,
|
|
389
|
+
hasCamera: media.finalHasCamera,
|
|
390
|
+
failureReason: null,
|
|
391
|
+
uploadProgress: 100,
|
|
392
|
+
updatedAt: now,
|
|
393
|
+
})
|
|
394
|
+
.where(
|
|
395
|
+
and(
|
|
396
|
+
eq(schema.recordings.id, id),
|
|
397
|
+
ownerEmailMatches(schema.recordings.ownerEmail, ownerEmail),
|
|
398
|
+
eq(schema.recordings.status, "processing"),
|
|
399
|
+
isNull(schema.recordings.trashedAt),
|
|
400
|
+
),
|
|
401
|
+
)
|
|
402
|
+
.returning({ id: schema.recordings.id });
|
|
403
|
+
if (persisted.length !== 1) return false;
|
|
404
|
+
|
|
405
|
+
await writeAppState(`recording-upload-${id}`, {
|
|
406
|
+
recordingId: id,
|
|
407
|
+
status: "processing",
|
|
408
|
+
progress: 100,
|
|
409
|
+
pendingMediaVerification: true,
|
|
410
|
+
mediaVerificationAttempt: retryAttempt,
|
|
411
|
+
mediaVerificationNextAttemptAt: nextAttemptAt,
|
|
412
|
+
mediaVerificationLastError: failureReason,
|
|
413
|
+
videoUrl: media.videoUrl,
|
|
414
|
+
videoSizeBytes: media.videoSizeBytes,
|
|
415
|
+
sourceSizeBytes: media.sourceSizeBytes,
|
|
416
|
+
videoFormat: media.videoFormat,
|
|
417
|
+
durationMs: media.finalDurationMs,
|
|
418
|
+
width: media.finalWidth,
|
|
419
|
+
height: media.finalHeight,
|
|
420
|
+
hasAudio: media.finalHasAudio,
|
|
421
|
+
hasCamera: media.finalHasCamera,
|
|
422
|
+
seekableApplied: media.seekableApplied,
|
|
423
|
+
mimeType: media.mimeType,
|
|
424
|
+
providerId: media.providerId,
|
|
425
|
+
assetDbId: media.assetDbId,
|
|
426
|
+
locallyTranscoded: media.locallyTranscoded,
|
|
427
|
+
updatedAt: now,
|
|
428
|
+
});
|
|
429
|
+
await writeAppState(mediaVerificationStateKey(id), {
|
|
430
|
+
recordingId: id,
|
|
431
|
+
status: "pending",
|
|
432
|
+
completedAttempts: retryAttempt,
|
|
433
|
+
nextAttemptAt,
|
|
434
|
+
leaseUntil: null,
|
|
435
|
+
updatedAt: now,
|
|
436
|
+
});
|
|
437
|
+
await writeAppState("refresh-signal", { ts: Date.now() });
|
|
438
|
+
return true;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
async function claimPendingMediaVerification(
|
|
442
|
+
id: string,
|
|
443
|
+
retryAttempt: number,
|
|
444
|
+
): Promise<boolean> {
|
|
445
|
+
const key = mediaVerificationStateKey(id);
|
|
446
|
+
const raw = await readAppState(key).catch(() => null);
|
|
447
|
+
const marker = parseMediaVerificationMarker(raw);
|
|
448
|
+
const now = Date.now();
|
|
449
|
+
if (
|
|
450
|
+
!marker ||
|
|
451
|
+
marker.recordingId !== id ||
|
|
452
|
+
retryAttempt !== marker.completedAttempts + 1 ||
|
|
453
|
+
now < Date.parse(marker.nextAttemptAt) ||
|
|
454
|
+
(marker.status === "leased" &&
|
|
455
|
+
marker.leaseUntil !== null &&
|
|
456
|
+
now < Date.parse(marker.leaseUntil))
|
|
457
|
+
) {
|
|
458
|
+
return false;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
const updatedAt = new Date(now).toISOString();
|
|
462
|
+
return compareAndSetAppState(key, raw as Record<string, unknown>, {
|
|
463
|
+
...marker,
|
|
464
|
+
status: "leased",
|
|
465
|
+
leaseUntil: new Date(now + 60_000).toISOString(),
|
|
466
|
+
updatedAt,
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
async function dispatchMediaVerificationRetry(
|
|
471
|
+
id: string,
|
|
472
|
+
retryAttempt: number,
|
|
473
|
+
): Promise<void> {
|
|
474
|
+
await dispatchPostFinalizeJob({
|
|
475
|
+
recordingId: id,
|
|
476
|
+
kind: "media-ready",
|
|
477
|
+
delayMs: mediaVerificationRetryDelayMs(retryAttempt),
|
|
478
|
+
retryAttempt,
|
|
479
|
+
requireAccepted: true,
|
|
480
|
+
}).catch((err: unknown) => {
|
|
481
|
+
console.error("[finalize] media verification dispatch failed", {
|
|
482
|
+
id,
|
|
483
|
+
retryAttempt,
|
|
484
|
+
error: err instanceof Error ? err.message : String(err),
|
|
485
|
+
});
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
async function leaveRecordingProcessingForMediaVerification(params: {
|
|
490
|
+
id: string;
|
|
491
|
+
ownerEmail: string;
|
|
492
|
+
media: PendingMediaVerification;
|
|
493
|
+
failureReason: string;
|
|
494
|
+
}) {
|
|
495
|
+
const persisted = await persistPendingMediaVerification(params);
|
|
496
|
+
if (!persisted) {
|
|
497
|
+
return {
|
|
498
|
+
id: params.id,
|
|
499
|
+
status: "failed" as const,
|
|
500
|
+
videoUrl: params.media.videoUrl,
|
|
501
|
+
videoSizeBytes: params.media.videoSizeBytes,
|
|
502
|
+
sourceSizeBytes: params.media.sourceSizeBytes,
|
|
503
|
+
durationMs: params.media.finalDurationMs,
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
await dispatchMediaVerificationRetry(params.id, 1);
|
|
507
|
+
return {
|
|
508
|
+
id: params.id,
|
|
509
|
+
status: "processing" as const,
|
|
510
|
+
verificationPending: true,
|
|
511
|
+
videoUrl: params.media.videoUrl,
|
|
512
|
+
videoSizeBytes: params.media.videoSizeBytes,
|
|
513
|
+
sourceSizeBytes: params.media.sourceSizeBytes,
|
|
514
|
+
durationMs: params.media.finalDurationMs,
|
|
515
|
+
};
|
|
276
516
|
}
|
|
277
517
|
|
|
278
518
|
// Flip recording to 'ready', seed transcript row, fire background transcript,
|
|
@@ -313,7 +553,7 @@ async function markRecordingReady(params: {
|
|
|
313
553
|
const db = getDb();
|
|
314
554
|
const now = new Date().toISOString();
|
|
315
555
|
|
|
316
|
-
await db
|
|
556
|
+
const promoted = await db
|
|
317
557
|
.update(schema.recordings)
|
|
318
558
|
.set({
|
|
319
559
|
status: "ready",
|
|
@@ -333,12 +573,10 @@ async function markRecordingReady(params: {
|
|
|
333
573
|
and(
|
|
334
574
|
eq(schema.recordings.id, id),
|
|
335
575
|
ownerEmailMatches(schema.recordings.ownerEmail, ownerEmail),
|
|
336
|
-
//
|
|
337
|
-
//
|
|
338
|
-
// the
|
|
339
|
-
|
|
340
|
-
// flipping 'failed' back to 'ready'.
|
|
341
|
-
ne(schema.recordings.status, "failed"),
|
|
576
|
+
// The processing -> ready transition is the idempotency fence for
|
|
577
|
+
// duplicate finalize requests and durable verification workers. Only
|
|
578
|
+
// the winner performs transcript/event/post-finalize side effects.
|
|
579
|
+
eq(schema.recordings.status, "processing"),
|
|
342
580
|
// Guard against the other direction of the cancel/finalize race:
|
|
343
581
|
// trash-recording's skipIfReady only blocks trashing a row that is
|
|
344
582
|
// ALREADY 'ready'. If cancel lands while this finalize is still
|
|
@@ -347,32 +585,30 @@ async function markRecordingReady(params: {
|
|
|
347
585
|
// 'ready' underneath a recording the user just trashed.
|
|
348
586
|
isNull(schema.recordings.trashedAt),
|
|
349
587
|
),
|
|
350
|
-
)
|
|
588
|
+
)
|
|
589
|
+
.returning({ id: schema.recordings.id });
|
|
351
590
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
),
|
|
367
|
-
);
|
|
368
|
-
if (!postUpdate || postUpdate.status !== "ready") {
|
|
369
|
-
debugLog(
|
|
370
|
-
"[finalize] markRecordingReady blocked — recording was aborted concurrently",
|
|
371
|
-
{ id, status: postUpdate?.status },
|
|
372
|
-
);
|
|
591
|
+
if (promoted.length !== 1) {
|
|
592
|
+
const [postUpdate] = await db
|
|
593
|
+
.select({ status: schema.recordings.status })
|
|
594
|
+
.from(schema.recordings)
|
|
595
|
+
.where(
|
|
596
|
+
and(
|
|
597
|
+
eq(schema.recordings.id, id),
|
|
598
|
+
ownerEmailMatches(schema.recordings.ownerEmail, ownerEmail),
|
|
599
|
+
),
|
|
600
|
+
);
|
|
601
|
+
debugLog("[finalize] markRecordingReady transition already resolved", {
|
|
602
|
+
id,
|
|
603
|
+
status: postUpdate?.status,
|
|
604
|
+
});
|
|
373
605
|
return {
|
|
374
606
|
id,
|
|
375
|
-
status:
|
|
607
|
+
status:
|
|
608
|
+
postUpdate?.status === "ready"
|
|
609
|
+
? ("ready" as const)
|
|
610
|
+
: ("failed" as const),
|
|
611
|
+
transitionedToReady: false,
|
|
376
612
|
videoUrl,
|
|
377
613
|
videoSizeBytes,
|
|
378
614
|
sourceSizeBytes,
|
|
@@ -404,6 +640,12 @@ async function markRecordingReady(params: {
|
|
|
404
640
|
durationMs: finalDurationMs,
|
|
405
641
|
finishedAt: now,
|
|
406
642
|
});
|
|
643
|
+
await deleteAppState(mediaVerificationStateKey(id)).catch((err) => {
|
|
644
|
+
console.warn("[finalize] failed to clear media verification marker", {
|
|
645
|
+
id,
|
|
646
|
+
error: err instanceof Error ? err.message : String(err),
|
|
647
|
+
});
|
|
648
|
+
});
|
|
407
649
|
await writeAppState("refresh-signal", { ts: Date.now() });
|
|
408
650
|
|
|
409
651
|
if (seekableApplied) {
|
|
@@ -464,6 +706,7 @@ async function markRecordingReady(params: {
|
|
|
464
706
|
return {
|
|
465
707
|
id,
|
|
466
708
|
status: "ready" as const,
|
|
709
|
+
transitionedToReady: true,
|
|
467
710
|
videoUrl,
|
|
468
711
|
videoSizeBytes,
|
|
469
712
|
sourceSizeBytes,
|
|
@@ -471,6 +714,165 @@ async function markRecordingReady(params: {
|
|
|
471
714
|
};
|
|
472
715
|
}
|
|
473
716
|
|
|
717
|
+
async function retryPendingMediaVerification(params: {
|
|
718
|
+
id: string;
|
|
719
|
+
ownerEmail: string;
|
|
720
|
+
existingTitle: string;
|
|
721
|
+
media: PendingMediaVerification;
|
|
722
|
+
retryAttempt: number;
|
|
723
|
+
}) {
|
|
724
|
+
const { id, ownerEmail, existingTitle, media, retryAttempt } = params;
|
|
725
|
+
const db = getDb();
|
|
726
|
+
const [recording] = await db
|
|
727
|
+
.select({
|
|
728
|
+
status: schema.recordings.status,
|
|
729
|
+
videoUrl: schema.recordings.videoUrl,
|
|
730
|
+
})
|
|
731
|
+
.from(schema.recordings)
|
|
732
|
+
.where(
|
|
733
|
+
and(
|
|
734
|
+
eq(schema.recordings.id, id),
|
|
735
|
+
ownerEmailMatches(schema.recordings.ownerEmail, ownerEmail),
|
|
736
|
+
),
|
|
737
|
+
);
|
|
738
|
+
if (!recording || recording.status !== "processing") {
|
|
739
|
+
return {
|
|
740
|
+
id,
|
|
741
|
+
status:
|
|
742
|
+
recording?.status === "ready"
|
|
743
|
+
? ("ready" as const)
|
|
744
|
+
: ("failed" as const),
|
|
745
|
+
videoUrl: recording?.videoUrl ?? media.videoUrl,
|
|
746
|
+
videoSizeBytes: media.videoSizeBytes,
|
|
747
|
+
sourceSizeBytes: media.sourceSizeBytes,
|
|
748
|
+
durationMs: media.finalDurationMs,
|
|
749
|
+
};
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
const candidate: PendingMediaVerification = {
|
|
753
|
+
...media,
|
|
754
|
+
videoUrl: recording.videoUrl || media.videoUrl,
|
|
755
|
+
};
|
|
756
|
+
try {
|
|
757
|
+
const servedBytes = await verifyServedMediaUrl(candidate.videoUrl);
|
|
758
|
+
const result = await markRecordingReady({
|
|
759
|
+
id,
|
|
760
|
+
ownerEmail,
|
|
761
|
+
existingTitle,
|
|
762
|
+
videoUrl: candidate.videoUrl,
|
|
763
|
+
videoSizeBytes: servedBytes ?? candidate.videoSizeBytes,
|
|
764
|
+
sourceSizeBytes: candidate.sourceSizeBytes,
|
|
765
|
+
videoFormat: candidate.videoFormat,
|
|
766
|
+
finalDurationMs: candidate.finalDurationMs,
|
|
767
|
+
finalWidth: candidate.finalWidth,
|
|
768
|
+
finalHeight: candidate.finalHeight,
|
|
769
|
+
finalHasAudio: candidate.finalHasAudio,
|
|
770
|
+
finalHasCamera: candidate.finalHasCamera,
|
|
771
|
+
seekableApplied: candidate.seekableApplied,
|
|
772
|
+
});
|
|
773
|
+
if (result.status === "ready" && result.transitionedToReady) {
|
|
774
|
+
queueBackgroundBuilderCompression({
|
|
775
|
+
recordingId: id,
|
|
776
|
+
ownerEmail,
|
|
777
|
+
videoUrl: candidate.videoUrl,
|
|
778
|
+
mimeType: candidate.mimeType,
|
|
779
|
+
providerId: candidate.providerId,
|
|
780
|
+
assetDbId: candidate.assetDbId,
|
|
781
|
+
sourceSizeBytes: candidate.videoSizeBytes,
|
|
782
|
+
locallyTranscoded: candidate.locallyTranscoded,
|
|
783
|
+
});
|
|
784
|
+
}
|
|
785
|
+
return result;
|
|
786
|
+
} catch (err) {
|
|
787
|
+
const failureReason = err instanceof Error ? err.message : String(err);
|
|
788
|
+
if (retryAttempt >= MEDIA_VERIFICATION_MAX_DURABLE_ATTEMPTS) {
|
|
789
|
+
const terminalReason = `${failureReason} after ${retryAttempt} durable verification attempts`;
|
|
790
|
+
const failed = await failStoredButUnservableRecording({
|
|
791
|
+
id,
|
|
792
|
+
ownerEmail,
|
|
793
|
+
failureReason: terminalReason,
|
|
794
|
+
});
|
|
795
|
+
if (!failed) {
|
|
796
|
+
const [resolved] = await db
|
|
797
|
+
.select({
|
|
798
|
+
status: schema.recordings.status,
|
|
799
|
+
videoUrl: schema.recordings.videoUrl,
|
|
800
|
+
})
|
|
801
|
+
.from(schema.recordings)
|
|
802
|
+
.where(
|
|
803
|
+
and(
|
|
804
|
+
eq(schema.recordings.id, id),
|
|
805
|
+
ownerEmailMatches(schema.recordings.ownerEmail, ownerEmail),
|
|
806
|
+
),
|
|
807
|
+
);
|
|
808
|
+
return {
|
|
809
|
+
id,
|
|
810
|
+
status:
|
|
811
|
+
resolved?.status === "ready"
|
|
812
|
+
? ("ready" as const)
|
|
813
|
+
: ("failed" as const),
|
|
814
|
+
videoUrl: resolved?.videoUrl ?? candidate.videoUrl,
|
|
815
|
+
videoSizeBytes: candidate.videoSizeBytes,
|
|
816
|
+
sourceSizeBytes: candidate.sourceSizeBytes,
|
|
817
|
+
durationMs: candidate.finalDurationMs,
|
|
818
|
+
};
|
|
819
|
+
}
|
|
820
|
+
return {
|
|
821
|
+
id,
|
|
822
|
+
status: "failed" as const,
|
|
823
|
+
videoUrl: candidate.videoUrl,
|
|
824
|
+
videoSizeBytes: candidate.videoSizeBytes,
|
|
825
|
+
sourceSizeBytes: candidate.sourceSizeBytes,
|
|
826
|
+
durationMs: candidate.finalDurationMs,
|
|
827
|
+
failureReason: terminalReason,
|
|
828
|
+
};
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
const persisted = await persistPendingMediaVerification({
|
|
832
|
+
id,
|
|
833
|
+
ownerEmail,
|
|
834
|
+
media: candidate,
|
|
835
|
+
failureReason,
|
|
836
|
+
retryAttempt,
|
|
837
|
+
});
|
|
838
|
+
if (!persisted) {
|
|
839
|
+
const [resolved] = await db
|
|
840
|
+
.select({
|
|
841
|
+
status: schema.recordings.status,
|
|
842
|
+
videoUrl: schema.recordings.videoUrl,
|
|
843
|
+
})
|
|
844
|
+
.from(schema.recordings)
|
|
845
|
+
.where(
|
|
846
|
+
and(
|
|
847
|
+
eq(schema.recordings.id, id),
|
|
848
|
+
ownerEmailMatches(schema.recordings.ownerEmail, ownerEmail),
|
|
849
|
+
),
|
|
850
|
+
);
|
|
851
|
+
return {
|
|
852
|
+
id,
|
|
853
|
+
status:
|
|
854
|
+
resolved?.status === "ready"
|
|
855
|
+
? ("ready" as const)
|
|
856
|
+
: ("failed" as const),
|
|
857
|
+
videoUrl: resolved?.videoUrl ?? candidate.videoUrl,
|
|
858
|
+
videoSizeBytes: candidate.videoSizeBytes,
|
|
859
|
+
sourceSizeBytes: candidate.sourceSizeBytes,
|
|
860
|
+
durationMs: candidate.finalDurationMs,
|
|
861
|
+
};
|
|
862
|
+
}
|
|
863
|
+
await dispatchMediaVerificationRetry(id, retryAttempt + 1);
|
|
864
|
+
return {
|
|
865
|
+
id,
|
|
866
|
+
status: "processing" as const,
|
|
867
|
+
verificationPending: true,
|
|
868
|
+
videoUrl: candidate.videoUrl,
|
|
869
|
+
videoSizeBytes: candidate.videoSizeBytes,
|
|
870
|
+
sourceSizeBytes: candidate.sourceSizeBytes,
|
|
871
|
+
durationMs: candidate.finalDurationMs,
|
|
872
|
+
};
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
|
|
474
876
|
export default defineAction({
|
|
475
877
|
description:
|
|
476
878
|
"Assemble recorded chunks into a final video blob, upload it to the configured storage provider, update the recording row (videoUrl, durationMs, width/height/hasAudio/hasCamera), flip status to 'ready', and trigger the agent to produce a title, summary, transcript, and chapters in the background.",
|
|
@@ -499,6 +901,7 @@ export default defineAction({
|
|
|
499
901
|
.describe(
|
|
500
902
|
"Whether the uploaded video bytes were already locally transcoded/compressed before upload",
|
|
501
903
|
),
|
|
904
|
+
mediaVerificationRetryAttempt: z.number().int().min(1).max(10).optional(),
|
|
502
905
|
}),
|
|
503
906
|
run: async (args) => {
|
|
504
907
|
const db = getDb();
|
|
@@ -547,6 +950,7 @@ export default defineAction({
|
|
|
547
950
|
await deleteResumableSession(id).catch((err) =>
|
|
548
951
|
console.warn("[finalize] failed to delete resumable session:", err),
|
|
549
952
|
);
|
|
953
|
+
await deleteAppState(mediaVerificationStateKey(id)).catch(() => {});
|
|
550
954
|
return {
|
|
551
955
|
id,
|
|
552
956
|
status: "ready" as const,
|
|
@@ -605,6 +1009,33 @@ export default defineAction({
|
|
|
605
1009
|
existingTitle: existing.title,
|
|
606
1010
|
};
|
|
607
1011
|
|
|
1012
|
+
const pendingMedia = pendingMediaVerificationFromState(uploadState);
|
|
1013
|
+
if (existing.status === "processing" && pendingMedia) {
|
|
1014
|
+
const retryAttempt =
|
|
1015
|
+
args.mediaVerificationRetryAttempt ??
|
|
1016
|
+
stateNumber(uploadState, "mediaVerificationAttempt") ??
|
|
1017
|
+
1;
|
|
1018
|
+
const claimed = await claimPendingMediaVerification(id, retryAttempt);
|
|
1019
|
+
if (!claimed) {
|
|
1020
|
+
return {
|
|
1021
|
+
id,
|
|
1022
|
+
status: "processing" as const,
|
|
1023
|
+
verificationPending: true,
|
|
1024
|
+
videoUrl: pendingMedia.videoUrl,
|
|
1025
|
+
videoSizeBytes: pendingMedia.videoSizeBytes,
|
|
1026
|
+
sourceSizeBytes: pendingMedia.sourceSizeBytes,
|
|
1027
|
+
durationMs: pendingMedia.finalDurationMs,
|
|
1028
|
+
};
|
|
1029
|
+
}
|
|
1030
|
+
return retryPendingMediaVerification({
|
|
1031
|
+
id,
|
|
1032
|
+
ownerEmail,
|
|
1033
|
+
existingTitle: existing.title,
|
|
1034
|
+
media: pendingMedia,
|
|
1035
|
+
retryAttempt,
|
|
1036
|
+
});
|
|
1037
|
+
}
|
|
1038
|
+
|
|
608
1039
|
// Resumable path: create-recording initialized a session and chunk.post.ts
|
|
609
1040
|
// forwarded all chunks to the provider. Complete the session to get the CDN URL.
|
|
610
1041
|
const resumableSession = await getResumableSession(id);
|
|
@@ -651,6 +1082,24 @@ export default defineAction({
|
|
|
651
1082
|
updatedAt: recoveryStartedAt,
|
|
652
1083
|
});
|
|
653
1084
|
}
|
|
1085
|
+
if (existing.status !== "processing" && existing.status !== "failed") {
|
|
1086
|
+
const processingStartedAt = new Date().toISOString();
|
|
1087
|
+
await db
|
|
1088
|
+
.update(schema.recordings)
|
|
1089
|
+
.set({
|
|
1090
|
+
status: "processing",
|
|
1091
|
+
failureReason: null,
|
|
1092
|
+
uploadProgress: 100,
|
|
1093
|
+
updatedAt: processingStartedAt,
|
|
1094
|
+
})
|
|
1095
|
+
.where(
|
|
1096
|
+
and(
|
|
1097
|
+
eq(schema.recordings.id, id),
|
|
1098
|
+
ownerEmailMatches(schema.recordings.ownerEmail, ownerEmail),
|
|
1099
|
+
eq(schema.recordings.status, existing.status),
|
|
1100
|
+
),
|
|
1101
|
+
);
|
|
1102
|
+
}
|
|
654
1103
|
try {
|
|
655
1104
|
const uploadProvider = await resolveResumableUploadProvider(
|
|
656
1105
|
resumableSession.providerId,
|
|
@@ -672,32 +1121,51 @@ export default defineAction({
|
|
|
672
1121
|
{ stableUrl: true, recordAsset: false },
|
|
673
1122
|
);
|
|
674
1123
|
debugLog("[finalize] resumable upload completed", { id, videoUrl });
|
|
1124
|
+
let servedBytes: number | null;
|
|
675
1125
|
try {
|
|
676
|
-
await verifyServedMediaUrl(
|
|
677
|
-
videoUrl,
|
|
678
|
-
resumableSession.bytesUploaded,
|
|
679
|
-
);
|
|
1126
|
+
servedBytes = await verifyServedMediaUrl(videoUrl);
|
|
680
1127
|
} catch (err) {
|
|
681
1128
|
const failureReason =
|
|
682
1129
|
err instanceof Error ? err.message : String(err);
|
|
683
|
-
await
|
|
1130
|
+
const pending = await leaveRecordingProcessingForMediaVerification({
|
|
684
1131
|
id,
|
|
685
1132
|
ownerEmail,
|
|
686
1133
|
failureReason,
|
|
1134
|
+
media: {
|
|
1135
|
+
videoUrl,
|
|
1136
|
+
videoSizeBytes: resumableSession.bytesUploaded,
|
|
1137
|
+
sourceSizeBytes: resumableSession.bytesUploaded,
|
|
1138
|
+
videoFormat,
|
|
1139
|
+
finalDurationMs,
|
|
1140
|
+
finalWidth,
|
|
1141
|
+
finalHeight,
|
|
1142
|
+
finalHasAudio,
|
|
1143
|
+
finalHasCamera,
|
|
1144
|
+
seekableApplied: false,
|
|
1145
|
+
mimeType,
|
|
1146
|
+
providerId: resumableSession.providerId,
|
|
1147
|
+
locallyTranscoded: args.locallyTranscoded === true,
|
|
1148
|
+
},
|
|
687
1149
|
});
|
|
688
|
-
|
|
1150
|
+
await deleteResumableSession(id).catch((deleteErr) =>
|
|
1151
|
+
console.warn(
|
|
1152
|
+
"[finalize] failed to retire pending resumable session:",
|
|
1153
|
+
deleteErr,
|
|
1154
|
+
),
|
|
1155
|
+
);
|
|
1156
|
+
return pending;
|
|
689
1157
|
}
|
|
690
1158
|
const result = await markRecordingReady({
|
|
691
1159
|
...readyParams,
|
|
692
1160
|
videoUrl,
|
|
693
|
-
videoSizeBytes: resumableSession.bytesUploaded,
|
|
1161
|
+
videoSizeBytes: servedBytes ?? resumableSession.bytesUploaded,
|
|
694
1162
|
sourceSizeBytes: resumableSession.bytesUploaded,
|
|
695
1163
|
// Streaming path forwards raw MediaRecorder bytes straight to the
|
|
696
1164
|
// provider — no faststart/Cues rewrite happened. Repair in the
|
|
697
1165
|
// background.
|
|
698
1166
|
seekableApplied: false,
|
|
699
1167
|
});
|
|
700
|
-
if (result.status === "ready") {
|
|
1168
|
+
if (result.status === "ready" && result.transitionedToReady) {
|
|
701
1169
|
queueBackgroundBuilderCompression({
|
|
702
1170
|
recordingId: id,
|
|
703
1171
|
ownerEmail,
|
|
@@ -1197,22 +1665,33 @@ export default defineAction({
|
|
|
1197
1665
|
videoUrl: upload.url,
|
|
1198
1666
|
bytes: uploadData.byteLength,
|
|
1199
1667
|
});
|
|
1668
|
+
let servedBytes: number | null;
|
|
1200
1669
|
try {
|
|
1201
|
-
await verifyServedMediaUrl(upload.url
|
|
1670
|
+
servedBytes = await verifyServedMediaUrl(upload.url);
|
|
1202
1671
|
} catch (err) {
|
|
1203
|
-
if (!requiresConfiguredVideoStorage()) {
|
|
1204
|
-
// Local dev can keep scratch chunks so the user can retry after fixing
|
|
1205
|
-
// a local storage/server issue. Hosted SQL must not keep video blobs in
|
|
1206
|
-
// application_state after a provider returned an unservable URL.
|
|
1207
|
-
chunkKeysToPurge = [];
|
|
1208
|
-
}
|
|
1209
1672
|
const failureReason = err instanceof Error ? err.message : String(err);
|
|
1210
|
-
|
|
1673
|
+
return leaveRecordingProcessingForMediaVerification({
|
|
1211
1674
|
id,
|
|
1212
1675
|
ownerEmail,
|
|
1213
1676
|
failureReason,
|
|
1677
|
+
media: {
|
|
1678
|
+
videoUrl: upload.url,
|
|
1679
|
+
videoSizeBytes: uploadData.byteLength,
|
|
1680
|
+
sourceSizeBytes: assembled.byteLength,
|
|
1681
|
+
videoFormat,
|
|
1682
|
+
finalDurationMs,
|
|
1683
|
+
finalWidth,
|
|
1684
|
+
finalHeight,
|
|
1685
|
+
finalHasAudio: correctedHasAudio,
|
|
1686
|
+
finalHasCamera,
|
|
1687
|
+
seekableApplied,
|
|
1688
|
+
mimeType,
|
|
1689
|
+
providerId: upload.provider,
|
|
1690
|
+
assetDbId: upload.id,
|
|
1691
|
+
locallyTranscoded:
|
|
1692
|
+
args.locallyTranscoded === true || Boolean(compressionMeta),
|
|
1693
|
+
},
|
|
1214
1694
|
});
|
|
1215
|
-
throw err;
|
|
1216
1695
|
}
|
|
1217
1696
|
const result = await markRecordingReady({
|
|
1218
1697
|
...readyParams,
|
|
@@ -1220,11 +1699,11 @@ export default defineAction({
|
|
|
1220
1699
|
// `readyParams` — see the audio sanity check above.
|
|
1221
1700
|
finalHasAudio: correctedHasAudio,
|
|
1222
1701
|
videoUrl: upload.url,
|
|
1223
|
-
videoSizeBytes: uploadData.byteLength,
|
|
1702
|
+
videoSizeBytes: servedBytes ?? uploadData.byteLength,
|
|
1224
1703
|
sourceSizeBytes: assembled.byteLength,
|
|
1225
1704
|
seekableApplied,
|
|
1226
1705
|
});
|
|
1227
|
-
if (result.status === "ready") {
|
|
1706
|
+
if (result.status === "ready" && result.transitionedToReady) {
|
|
1228
1707
|
queueBackgroundBuilderCompression({
|
|
1229
1708
|
recordingId: id,
|
|
1230
1709
|
ownerEmail,
|