@agent-native/core 0.109.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 +7 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/a2a/client.ts +24 -2
- package/corpus/core/src/a2a/correlation.ts +50 -0
- package/corpus/core/src/a2a/handlers.ts +140 -14
- package/corpus/core/src/a2a/index.ts +1 -0
- package/corpus/core/src/a2a/invoke.ts +10 -1
- package/corpus/core/src/a2a/task-store.ts +146 -6
- package/corpus/core/src/a2a/types.ts +16 -0
- package/corpus/core/src/action.ts +2 -0
- package/corpus/core/src/agent/production-agent.ts +23 -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/observability/traces.ts +172 -19
- package/corpus/core/src/scripts/call-agent.ts +53 -2
- package/corpus/core/src/server/agent-chat/action-filters-a2a.ts +72 -25
- package/corpus/core/src/server/agent-chat-plugin.ts +107 -6
- package/corpus/core/src/templates/workspace-core/.agents/skills/a2a-protocol/SKILL.md +21 -0
- package/corpus/core/src/templates/workspace-core/.agents/skills/observability/SKILL.md +7 -1
- package/corpus/core/src/templates/workspace-core/.agents/skills/tracking/SKILL.md +1 -1
- package/corpus/templates/analytics/actions/query-agent-native-analytics.ts +1 -1
- 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/docs/schemas/first-party-analytics.md +23 -14
- package/corpus/templates/analytics/docs/schemas/tracked-events.md +5 -1
- package/corpus/templates/analytics/server/jobs/analytics-alerts.ts +3 -3
- package/corpus/templates/analytics/server/lib/analytics-alerts.ts +209 -87
- package/corpus/templates/clips/actions/finalize-recording.ts +541 -62
- 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/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-clips-now-retries-storage-verification-in-the-background-ins.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 +18 -6
- 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/dist/a2a/client.d.ts +11 -2
- package/dist/a2a/client.d.ts.map +1 -1
- package/dist/a2a/client.js +16 -3
- 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 +100 -15
- package/dist/a2a/handlers.js.map +1 -1
- package/dist/a2a/index.d.ts +1 -1
- package/dist/a2a/index.d.ts.map +1 -1
- package/dist/a2a/index.js.map +1 -1
- package/dist/a2a/invoke.d.ts +4 -1
- package/dist/a2a/invoke.d.ts.map +1 -1
- package/dist/a2a/invoke.js +3 -0
- package/dist/a2a/invoke.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 +15 -0
- package/dist/a2a/types.d.ts.map +1 -1
- package/dist/a2a/types.js.map +1 -1
- package/dist/action.d.ts +2 -0
- package/dist/action.d.ts.map +1 -1
- package/dist/action.js.map +1 -1
- package/dist/agent/production-agent.d.ts +9 -0
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +14 -0
- package/dist/agent/production-agent.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/collab/routes.d.ts +1 -1
- package/dist/collab/struct-routes.d.ts +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/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/resources/handlers.d.ts +2 -2
- package/dist/scripts/call-agent.d.ts.map +1 -1
- package/dist/scripts/call-agent.js +35 -3
- 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 +18 -2
- package/dist/server/agent-chat/action-filters-a2a.d.ts.map +1 -1
- package/dist/server/agent-chat/action-filters-a2a.js +35 -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 +93 -6
- package/dist/server/agent-chat-plugin.js.map +1 -1
- package/dist/templates/workspace-core/.agents/skills/a2a-protocol/SKILL.md +21 -0
- package/dist/templates/workspace-core/.agents/skills/observability/SKILL.md +7 -1
- package/dist/templates/workspace-core/.agents/skills/tracking/SKILL.md +1 -1
- package/package.json +1 -1
- package/src/templates/workspace-core/.agents/skills/a2a-protocol/SKILL.md +21 -0
- package/src/templates/workspace-core/.agents/skills/observability/SKILL.md +7 -1
- package/src/templates/workspace-core/.agents/skills/tracking/SKILL.md +1 -1
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
export const PLAYBACK_COMMENT_VISIBLE_MS = 4_000;
|
|
2
2
|
|
|
3
|
+
export function getPlaybackCommentVisibleMs(playbackRate = 1): number {
|
|
4
|
+
const safePlaybackRate =
|
|
5
|
+
Number.isFinite(playbackRate) && playbackRate > 0 ? playbackRate : 1;
|
|
6
|
+
return PLAYBACK_COMMENT_VISIBLE_MS * safePlaybackRate;
|
|
7
|
+
}
|
|
8
|
+
|
|
3
9
|
export interface PlaybackComment {
|
|
4
10
|
id: string;
|
|
5
11
|
content: string;
|
|
@@ -13,11 +19,14 @@ export interface PlaybackComment {
|
|
|
13
19
|
export function getActivePlaybackComments(
|
|
14
20
|
comments: PlaybackComment[] | undefined,
|
|
15
21
|
currentMs: number,
|
|
22
|
+
playbackRate = 1,
|
|
16
23
|
): PlaybackComment[] {
|
|
17
24
|
if (!comments?.length || !Number.isFinite(currentMs) || currentMs < 0) {
|
|
18
25
|
return [];
|
|
19
26
|
}
|
|
20
27
|
|
|
28
|
+
const visibleMs = getPlaybackCommentVisibleMs(playbackRate);
|
|
29
|
+
|
|
21
30
|
return comments
|
|
22
31
|
.filter((comment) => {
|
|
23
32
|
const timestamp = comment.videoTimestampMs;
|
|
@@ -28,7 +37,7 @@ export function getActivePlaybackComments(
|
|
|
28
37
|
Number.isFinite(timestamp) &&
|
|
29
38
|
timestamp >= 0 &&
|
|
30
39
|
currentMs >= timestamp &&
|
|
31
|
-
currentMs < timestamp +
|
|
40
|
+
currentMs < timestamp + visibleMs
|
|
32
41
|
);
|
|
33
42
|
})
|
|
34
43
|
.sort(
|
|
@@ -40,11 +49,17 @@ export function getActivePlaybackComments(
|
|
|
40
49
|
export function PlaybackCommentOverlay({
|
|
41
50
|
comments,
|
|
42
51
|
currentMs,
|
|
52
|
+
playbackRate = 1,
|
|
43
53
|
}: {
|
|
44
54
|
comments: PlaybackComment[] | undefined;
|
|
45
55
|
currentMs: number;
|
|
56
|
+
playbackRate?: number;
|
|
46
57
|
}) {
|
|
47
|
-
const activeComments = getActivePlaybackComments(
|
|
58
|
+
const activeComments = getActivePlaybackComments(
|
|
59
|
+
comments,
|
|
60
|
+
currentMs,
|
|
61
|
+
playbackRate,
|
|
62
|
+
);
|
|
48
63
|
if (activeComments.length === 0) return null;
|
|
49
64
|
|
|
50
65
|
return (
|
|
@@ -318,6 +318,7 @@ export const VideoPlayer = forwardRef<VideoPlayerHandle, VideoPlayerProps>(
|
|
|
318
318
|
// Whether we've already captured-and-uploaded a still-frame thumbnail for
|
|
319
319
|
// this clip. Owner-only and once per player lifecycle.
|
|
320
320
|
const thumbnailCapturedRef = useRef(false);
|
|
321
|
+
const [thumbnailLoadFailed, setThumbnailLoadFailed] = useState(false);
|
|
321
322
|
// "Preparing your clip…" overlay — shown while the browser buffers the
|
|
322
323
|
// first frame of a freshly-finalized clip so the user doesn't see a blank
|
|
323
324
|
// black rectangle. Hidden on loadeddata / canplay / currentTime > 0, or
|
|
@@ -933,6 +934,10 @@ export const VideoPlayer = forwardRef<VideoPlayerHandle, VideoPlayerProps>(
|
|
|
933
934
|
setPlayError(null);
|
|
934
935
|
}, [activeVideoSourceIdentity, recordingId]);
|
|
935
936
|
|
|
937
|
+
useEffect(() => {
|
|
938
|
+
setThumbnailLoadFailed(false);
|
|
939
|
+
}, [recordingId, thumbnailUrl]);
|
|
940
|
+
|
|
936
941
|
useEffect(() => {
|
|
937
942
|
let cancelled = false;
|
|
938
943
|
setShouldRefreshAutoThumbnail(false);
|
|
@@ -1188,10 +1193,11 @@ export const VideoPlayer = forwardRef<VideoPlayerHandle, VideoPlayerProps>(
|
|
|
1188
1193
|
// retry on a format that will never play. Show the poster with a
|
|
1189
1194
|
// clear, non-looping explanation instead.
|
|
1190
1195
|
<div className="relative flex h-full w-full items-center justify-center bg-black">
|
|
1191
|
-
{thumbnailUrl ? (
|
|
1196
|
+
{thumbnailUrl && !thumbnailLoadFailed ? (
|
|
1192
1197
|
<img
|
|
1193
1198
|
src={resolveLocalUrl(thumbnailUrl)}
|
|
1194
1199
|
alt=""
|
|
1200
|
+
onError={() => setThumbnailLoadFailed(true)}
|
|
1195
1201
|
className={cn(
|
|
1196
1202
|
"absolute inset-0 h-full w-full",
|
|
1197
1203
|
cover ? "object-cover" : "object-contain",
|
|
@@ -1447,6 +1453,7 @@ export const VideoPlayer = forwardRef<VideoPlayerHandle, VideoPlayerProps>(
|
|
|
1447
1453
|
)}
|
|
1448
1454
|
|
|
1449
1455
|
{thumbnailUrl &&
|
|
1456
|
+
!thumbnailLoadFailed &&
|
|
1450
1457
|
!autoPlay &&
|
|
1451
1458
|
!hasPlaybackStarted &&
|
|
1452
1459
|
(!startMs || startMs <= 0) ? (
|
|
@@ -1454,6 +1461,7 @@ export const VideoPlayer = forwardRef<VideoPlayerHandle, VideoPlayerProps>(
|
|
|
1454
1461
|
src={resolveLocalUrl(thumbnailUrl)}
|
|
1455
1462
|
alt=""
|
|
1456
1463
|
aria-hidden="true"
|
|
1464
|
+
onError={() => setThumbnailLoadFailed(true)}
|
|
1457
1465
|
className={cn(
|
|
1458
1466
|
"pointer-events-none absolute inset-0 z-[1] h-full w-full",
|
|
1459
1467
|
cover ? "object-cover" : "object-contain",
|
|
@@ -1495,7 +1503,11 @@ export const VideoPlayer = forwardRef<VideoPlayerHandle, VideoPlayerProps>(
|
|
|
1495
1503
|
|
|
1496
1504
|
{/* Timestamped comments */}
|
|
1497
1505
|
{!hideChrome && !isLoomEmbed && hasPlaybackStarted ? (
|
|
1498
|
-
<PlaybackCommentOverlay
|
|
1506
|
+
<PlaybackCommentOverlay
|
|
1507
|
+
comments={comments}
|
|
1508
|
+
currentMs={currentMs}
|
|
1509
|
+
playbackRate={speed}
|
|
1510
|
+
/>
|
|
1499
1511
|
) : null}
|
|
1500
1512
|
|
|
1501
1513
|
{/* Floating CTA (throughout placement) */}
|
|
@@ -11,7 +11,7 @@ import {
|
|
|
11
11
|
captureClientException,
|
|
12
12
|
trackEvent,
|
|
13
13
|
} from "@agent-native/core/client";
|
|
14
|
-
import {
|
|
14
|
+
import { waitForAcceptedRecordingAfterFinalizeError } from "@shared/finalize-recovery";
|
|
15
15
|
import {
|
|
16
16
|
chooseFallbackAudioInput,
|
|
17
17
|
enumerateAudioInputDevices,
|
|
@@ -2231,7 +2231,7 @@ export class RecorderEngine {
|
|
|
2231
2231
|
private async recoverReadyAfterFinalUploadError(
|
|
2232
2232
|
signal?: AbortSignal,
|
|
2233
2233
|
): Promise<Record<string, unknown> | null> {
|
|
2234
|
-
return
|
|
2234
|
+
return waitForAcceptedRecordingAfterFinalizeError({
|
|
2235
2235
|
uploadUrl: this.opts.uploadUrl,
|
|
2236
2236
|
recordingId: this.opts.recordingId,
|
|
2237
2237
|
preferAuthenticated: true,
|
|
@@ -92,7 +92,7 @@ import { cn } from "@/lib/utils";
|
|
|
92
92
|
import { STALE_PENDING_TRANSCRIPT_REASON } from "../../shared/transcript-status";
|
|
93
93
|
|
|
94
94
|
const UPLOAD_STUCK_TIMEOUT_MS = 5 * 60 * 1000;
|
|
95
|
-
const PROCESSING_STUCK_TIMEOUT_MS =
|
|
95
|
+
const PROCESSING_STUCK_TIMEOUT_MS = 12 * 60 * 1000;
|
|
96
96
|
const READY_MEDIA_SETTLE_POLL_MS = 20 * 1000;
|
|
97
97
|
const READY_MEDIA_SETTLE_POLL_INTERVAL_MS = 1000;
|
|
98
98
|
|
|
@@ -9,7 +9,7 @@ import { useLiveTranscription } from "@agent-native/core/client/transcription/us
|
|
|
9
9
|
import type { BrowserDiagnosticsData } from "@shared/browser-diagnostics";
|
|
10
10
|
import {
|
|
11
11
|
isStoredButUnservableFinalizeError,
|
|
12
|
-
|
|
12
|
+
waitForAcceptedRecordingAfterFinalizeError,
|
|
13
13
|
} from "@shared/finalize-recovery";
|
|
14
14
|
import {
|
|
15
15
|
chunkUploadParallelism,
|
|
@@ -1733,7 +1733,7 @@ export default function RecordRoute() {
|
|
|
1733
1733
|
createdId &&
|
|
1734
1734
|
(err as { name?: string } | null)?.name !== "AbortError"
|
|
1735
1735
|
) {
|
|
1736
|
-
const recovered = await
|
|
1736
|
+
const recovered = await waitForAcceptedRecordingAfterFinalizeError({
|
|
1737
1737
|
uploadUrl: uploadBase,
|
|
1738
1738
|
recordingId: createdId,
|
|
1739
1739
|
preferAuthenticated: true,
|
|
@@ -1764,7 +1764,7 @@ export default function RecordRoute() {
|
|
|
1764
1764
|
chunkRes.status !== 413 &&
|
|
1765
1765
|
!isUploadSizeError(error.message)
|
|
1766
1766
|
) {
|
|
1767
|
-
const recovered = await
|
|
1767
|
+
const recovered = await waitForAcceptedRecordingAfterFinalizeError({
|
|
1768
1768
|
uploadUrl: uploadBase,
|
|
1769
1769
|
recordingId: createdId,
|
|
1770
1770
|
preferAuthenticated: true,
|
|
@@ -67,6 +67,7 @@ import { parsePlaybackSpeed } from "@/lib/playback-speed";
|
|
|
67
67
|
import { isStorageSetupFailureReason } from "@/lib/storage-failures";
|
|
68
68
|
|
|
69
69
|
import { getDb, schema } from "../../server/db";
|
|
70
|
+
import { resolvePlayerThumbnailUrl } from "../../server/lib/player-thumbnail-url";
|
|
70
71
|
import {
|
|
71
72
|
buildAgentApiUrls,
|
|
72
73
|
CLIPS_AGENT_ACCESS_PARAM,
|
|
@@ -226,8 +227,10 @@ export async function loader({ params, url }: LoaderFunctionArgs) {
|
|
|
226
227
|
id: rec.id,
|
|
227
228
|
title: rec.title,
|
|
228
229
|
description: rec.description,
|
|
229
|
-
thumbnailUrl: rec.
|
|
230
|
-
|
|
230
|
+
thumbnailUrl: rec.password
|
|
231
|
+
? null
|
|
232
|
+
: resolvePlayerThumbnailUrl(rec, { appPath }),
|
|
233
|
+
animatedThumbnailUrl: null,
|
|
231
234
|
visibility: rec.visibility,
|
|
232
235
|
status: rec.status,
|
|
233
236
|
archivedAt: rec.archivedAt,
|
|
@@ -286,7 +289,7 @@ const CLIPS_TEMPLATE_URL = "https://www.agent-native.com/templates/clips";
|
|
|
286
289
|
const CLIPS_AGENT_DOCS_URL =
|
|
287
290
|
"https://www.agent-native.com/docs/template-clips#agent-readable-clips";
|
|
288
291
|
const UPLOAD_STUCK_TIMEOUT_MS = 5 * 60 * 1000;
|
|
289
|
-
const PROCESSING_STUCK_TIMEOUT_MS =
|
|
292
|
+
const PROCESSING_STUCK_TIMEOUT_MS = 12 * 60 * 1000;
|
|
290
293
|
|
|
291
294
|
type ViewerPlatform = "mac" | "windows" | "linux";
|
|
292
295
|
|
|
@@ -2,6 +2,9 @@ export {
|
|
|
2
2
|
authenticatedRecordingStatusUrl,
|
|
3
3
|
publicRecordingStatusUrl,
|
|
4
4
|
readyRecordingFromPublicPayload,
|
|
5
|
+
waitForAcceptedRecordingAfterFinalizeError,
|
|
5
6
|
waitForReadyRecordingAfterFinalizeError,
|
|
7
|
+
type RecoveredFinalizeRecording,
|
|
8
|
+
type RecoveredProcessingRecording,
|
|
6
9
|
type RecoveredReadyRecording,
|
|
7
10
|
} from "@shared/finalize-recovery";
|
|
@@ -35,7 +35,7 @@ import {
|
|
|
35
35
|
} from "@shared/recording-core";
|
|
36
36
|
import { MAX_UPLOAD_BYTES } from "@shared/upload-limits";
|
|
37
37
|
|
|
38
|
-
import {
|
|
38
|
+
import { waitForAcceptedRecordingAfterFinalizeError } from "./finalize-recovery";
|
|
39
39
|
import {
|
|
40
40
|
createNativeTranscriptionCapture,
|
|
41
41
|
type NativeTranscriptionCapture,
|
|
@@ -875,7 +875,7 @@ async function uploadChunk(
|
|
|
875
875
|
if (extra.isFinal && res.status === 504) {
|
|
876
876
|
triedFinalUploadRecovery = true;
|
|
877
877
|
await res.text().catch(() => "");
|
|
878
|
-
const recovered = await
|
|
878
|
+
const recovered = await waitForAcceptedRecordingAfterFinalizeError({
|
|
879
879
|
uploadUrl: recording.uploadUrl,
|
|
880
880
|
recordingId: recording.recordingId,
|
|
881
881
|
authToken: recording.authToken,
|
|
@@ -1619,7 +1619,7 @@ async function finalizeStop(recording: ActiveRecording): Promise<void> {
|
|
|
1619
1619
|
const error = err instanceof Error ? err : new Error(String(err));
|
|
1620
1620
|
|
|
1621
1621
|
if (isFinalUploadRecoveryCandidate(error)) {
|
|
1622
|
-
const recovered = await
|
|
1622
|
+
const recovered = await waitForAcceptedRecordingAfterFinalizeError({
|
|
1623
1623
|
uploadUrl: recording.uploadUrl,
|
|
1624
1624
|
recordingId: recording.recordingId,
|
|
1625
1625
|
authToken: recording.authToken,
|
|
@@ -70,6 +70,7 @@ import {
|
|
|
70
70
|
exportBrowserRecordingBackup,
|
|
71
71
|
listBrowserRecordingBackups,
|
|
72
72
|
retryBrowserRecordingBackup,
|
|
73
|
+
scheduleNativeBackupCleanupAfterProcessing,
|
|
73
74
|
shouldUseNativeFullscreenRecording,
|
|
74
75
|
startRecording,
|
|
75
76
|
type LocalExportedFile,
|
|
@@ -1894,12 +1895,23 @@ export function App() {
|
|
|
1894
1895
|
try {
|
|
1895
1896
|
const authToken = loadDesktopAuthToken(targetServerUrl);
|
|
1896
1897
|
if (upload.kind === "native") {
|
|
1897
|
-
await invoke(
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1898
|
+
const result = await invoke<{ verificationPending?: boolean }>(
|
|
1899
|
+
"native_fullscreen_recording_retry_upload",
|
|
1900
|
+
{
|
|
1901
|
+
serverUrl: targetServerUrl,
|
|
1902
|
+
recordingId: upload.recordingId,
|
|
1903
|
+
authToken,
|
|
1904
|
+
cookie:
|
|
1905
|
+
typeof document !== "undefined" ? document.cookie || "" : "",
|
|
1906
|
+
},
|
|
1907
|
+
);
|
|
1908
|
+
if (result.verificationPending) {
|
|
1909
|
+
scheduleNativeBackupCleanupAfterProcessing({
|
|
1910
|
+
serverUrl: targetServerUrl,
|
|
1911
|
+
recordingId: upload.recordingId,
|
|
1912
|
+
authToken,
|
|
1913
|
+
});
|
|
1914
|
+
}
|
|
1903
1915
|
} else {
|
|
1904
1916
|
await retryBrowserRecordingBackup({
|
|
1905
1917
|
recordingId: upload.recordingId,
|
|
@@ -51,7 +51,10 @@ import { invoke } from "@tauri-apps/api/core";
|
|
|
51
51
|
import { emit, listen, type UnlistenFn } from "@tauri-apps/api/event";
|
|
52
52
|
import { open as openExternal } from "@tauri-apps/plugin-shell";
|
|
53
53
|
|
|
54
|
-
import {
|
|
54
|
+
import {
|
|
55
|
+
waitForAcceptedRecordingAfterFinalizeError,
|
|
56
|
+
waitForReadyRecordingAfterFinalizeError,
|
|
57
|
+
} from "../../../shared/finalize-recovery";
|
|
55
58
|
import type { LocalRecordingMode } from "../shared/config";
|
|
56
59
|
import { createAudioCue, type AudioCue } from "./audio-cue";
|
|
57
60
|
import { createCameraCompositeStream } from "./camera-composite";
|
|
@@ -752,7 +755,56 @@ async function markBrowserRecordingBackupError(
|
|
|
752
755
|
});
|
|
753
756
|
}
|
|
754
757
|
|
|
755
|
-
|
|
758
|
+
function scheduleBrowserBackupCleanupAfterProcessing(args: {
|
|
759
|
+
serverUrl: string;
|
|
760
|
+
recordingId: string;
|
|
761
|
+
authToken?: string;
|
|
762
|
+
}): void {
|
|
763
|
+
void waitForReadyRecordingAfterFinalizeError({
|
|
764
|
+
uploadUrl: chunkUrl(args.serverUrl, args.recordingId, 0, false),
|
|
765
|
+
recordingId: args.recordingId,
|
|
766
|
+
authToken: args.authToken,
|
|
767
|
+
preferAuthenticated: true,
|
|
768
|
+
timeoutMs: 12 * 60 * 1000,
|
|
769
|
+
})
|
|
770
|
+
.then((recovered) => {
|
|
771
|
+
if (recovered?.status === "ready") {
|
|
772
|
+
return deleteBrowserRecordingBackup(args.recordingId);
|
|
773
|
+
}
|
|
774
|
+
})
|
|
775
|
+
.catch((err) => {
|
|
776
|
+
console.warn(
|
|
777
|
+
"[clips-recorder] background backup cleanup check failed:",
|
|
778
|
+
err,
|
|
779
|
+
);
|
|
780
|
+
});
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
export function scheduleNativeBackupCleanupAfterProcessing(args: {
|
|
784
|
+
serverUrl: string;
|
|
785
|
+
recordingId: string;
|
|
786
|
+
authToken?: string;
|
|
787
|
+
}): void {
|
|
788
|
+
void waitForReadyRecordingAfterFinalizeError({
|
|
789
|
+
uploadUrl: chunkUrl(args.serverUrl, args.recordingId, 0, false),
|
|
790
|
+
recordingId: args.recordingId,
|
|
791
|
+
authToken: args.authToken,
|
|
792
|
+
preferAuthenticated: true,
|
|
793
|
+
timeoutMs: 12 * 60 * 1000,
|
|
794
|
+
})
|
|
795
|
+
.then((recovered) => {
|
|
796
|
+
if (recovered?.status === "ready") {
|
|
797
|
+
return invoke("native_fullscreen_recording_dismiss_upload", {
|
|
798
|
+
recordingId: args.recordingId,
|
|
799
|
+
});
|
|
800
|
+
}
|
|
801
|
+
})
|
|
802
|
+
.catch((err) => {
|
|
803
|
+
console.warn("[clips-recorder] native backup cleanup check failed:", err);
|
|
804
|
+
});
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
async function recoverAcceptedRecordingAfterFinalizeError({
|
|
756
808
|
serverUrl,
|
|
757
809
|
recordingId,
|
|
758
810
|
authToken,
|
|
@@ -760,19 +812,27 @@ async function recoverReadyRecordingAfterFinalizeError({
|
|
|
760
812
|
serverUrl: string;
|
|
761
813
|
recordingId: string;
|
|
762
814
|
authToken?: string;
|
|
763
|
-
}): Promise<
|
|
764
|
-
const recovered = await
|
|
815
|
+
}): Promise<"processing" | "ready" | null> {
|
|
816
|
+
const recovered = await waitForAcceptedRecordingAfterFinalizeError({
|
|
765
817
|
uploadUrl: chunkUrl(serverUrl, recordingId, 0, false),
|
|
766
818
|
recordingId,
|
|
767
819
|
authToken,
|
|
768
820
|
preferAuthenticated: true,
|
|
769
821
|
});
|
|
770
|
-
if (!recovered) return
|
|
822
|
+
if (!recovered) return null;
|
|
823
|
+
if (recovered.status === "processing") {
|
|
824
|
+
scheduleBrowserBackupCleanupAfterProcessing({
|
|
825
|
+
serverUrl,
|
|
826
|
+
recordingId,
|
|
827
|
+
authToken,
|
|
828
|
+
});
|
|
829
|
+
return "processing";
|
|
830
|
+
}
|
|
771
831
|
await markBrowserRecordingBackupError(
|
|
772
832
|
recordingId,
|
|
773
833
|
"Upload completed, but its final receipt could not be verified. The local backup was kept.",
|
|
774
834
|
).catch(() => {});
|
|
775
|
-
return
|
|
835
|
+
return "ready";
|
|
776
836
|
}
|
|
777
837
|
|
|
778
838
|
export async function listBrowserRecordingBackups(): Promise<
|
|
@@ -951,10 +1011,21 @@ export async function retryBrowserRecordingBackup(input: {
|
|
|
951
1011
|
validatedChunks,
|
|
952
1012
|
input.authToken,
|
|
953
1013
|
);
|
|
954
|
-
verifyFinalizeReceipt(receipt, meta);
|
|
1014
|
+
const receiptStatus = verifyFinalizeReceipt(receipt, meta);
|
|
1015
|
+
if (receiptStatus === "processing") {
|
|
1016
|
+
scheduleBrowserBackupCleanupAfterProcessing({
|
|
1017
|
+
serverUrl: meta.serverUrl,
|
|
1018
|
+
recordingId: meta.recordingId,
|
|
1019
|
+
authToken: input.authToken,
|
|
1020
|
+
});
|
|
1021
|
+
return {
|
|
1022
|
+
recordingId: meta.recordingId,
|
|
1023
|
+
viewUrl: `/r/${meta.recordingId}`,
|
|
1024
|
+
};
|
|
1025
|
+
}
|
|
955
1026
|
} catch (err) {
|
|
956
1027
|
if (
|
|
957
|
-
await
|
|
1028
|
+
await recoverAcceptedRecordingAfterFinalizeError({
|
|
958
1029
|
serverUrl: meta.serverUrl,
|
|
959
1030
|
recordingId: meta.recordingId,
|
|
960
1031
|
authToken: input.authToken,
|
|
@@ -1007,10 +1078,21 @@ export async function retryBrowserRecordingBackup(input: {
|
|
|
1007
1078
|
new Blob([], { type: meta.mimeType }),
|
|
1008
1079
|
input.authToken,
|
|
1009
1080
|
);
|
|
1010
|
-
verifyFinalizeReceipt(receipt, meta);
|
|
1081
|
+
const receiptStatus = verifyFinalizeReceipt(receipt, meta);
|
|
1082
|
+
if (receiptStatus === "processing") {
|
|
1083
|
+
scheduleBrowserBackupCleanupAfterProcessing({
|
|
1084
|
+
serverUrl: meta.serverUrl,
|
|
1085
|
+
recordingId: meta.recordingId,
|
|
1086
|
+
authToken: input.authToken,
|
|
1087
|
+
});
|
|
1088
|
+
return {
|
|
1089
|
+
recordingId: meta.recordingId,
|
|
1090
|
+
viewUrl: `/r/${meta.recordingId}`,
|
|
1091
|
+
};
|
|
1092
|
+
}
|
|
1011
1093
|
} catch (err) {
|
|
1012
1094
|
if (
|
|
1013
|
-
await
|
|
1095
|
+
await recoverAcceptedRecordingAfterFinalizeError({
|
|
1014
1096
|
serverUrl: meta.serverUrl,
|
|
1015
1097
|
recordingId: meta.recordingId,
|
|
1016
1098
|
authToken: input.authToken,
|
|
@@ -1143,6 +1225,7 @@ interface NativeFullscreenUploadResult {
|
|
|
1143
1225
|
durationMs: number;
|
|
1144
1226
|
width?: number;
|
|
1145
1227
|
height?: number;
|
|
1228
|
+
verificationPending?: boolean;
|
|
1146
1229
|
}
|
|
1147
1230
|
|
|
1148
1231
|
interface NativeFullscreenSaveResult {
|
|
@@ -2490,7 +2573,7 @@ async function startNativeFullscreenRecording(
|
|
|
2490
2573
|
uploadResult = await uploadPromise;
|
|
2491
2574
|
} catch (err) {
|
|
2492
2575
|
if (
|
|
2493
|
-
await
|
|
2576
|
+
await recoverAcceptedRecordingAfterFinalizeError({
|
|
2494
2577
|
serverUrl: params.serverUrl,
|
|
2495
2578
|
recordingId: id,
|
|
2496
2579
|
authToken: params.authToken,
|
|
@@ -2505,6 +2588,13 @@ async function startNativeFullscreenRecording(
|
|
|
2505
2588
|
);
|
|
2506
2589
|
throw err;
|
|
2507
2590
|
}
|
|
2591
|
+
if (uploadResult.verificationPending) {
|
|
2592
|
+
scheduleNativeBackupCleanupAfterProcessing({
|
|
2593
|
+
serverUrl: params.serverUrl,
|
|
2594
|
+
recordingId: id,
|
|
2595
|
+
authToken: params.authToken,
|
|
2596
|
+
});
|
|
2597
|
+
}
|
|
2508
2598
|
const transcriptSaved = await transcriptSavePromise;
|
|
2509
2599
|
if (!transcriptSaved && capturedTranscript?.text.trim()) {
|
|
2510
2600
|
// The first write runs in parallel with native upload. Retry once
|
|
@@ -3878,15 +3968,23 @@ async function startRecordingInner(
|
|
|
3878
3968
|
const receipt = bodyText
|
|
3879
3969
|
? (JSON.parse(bodyText) as FinalizeReceipt)
|
|
3880
3970
|
: null;
|
|
3881
|
-
verifyFinalizeReceipt(receipt, {
|
|
3971
|
+
const receiptStatus = verifyFinalizeReceipt(receipt, {
|
|
3882
3972
|
bytes: backupBytes,
|
|
3883
3973
|
durationMs,
|
|
3884
3974
|
});
|
|
3975
|
+
if (receiptStatus === "processing") {
|
|
3976
|
+
scheduleBrowserBackupCleanupAfterProcessing({
|
|
3977
|
+
serverUrl: params.serverUrl,
|
|
3978
|
+
recordingId: id,
|
|
3979
|
+
authToken: params.authToken,
|
|
3980
|
+
});
|
|
3981
|
+
return { recordingId: id, viewUrl };
|
|
3982
|
+
}
|
|
3885
3983
|
} catch (err) {
|
|
3886
3984
|
console.error("[clips-recorder] finalize fetch failed:", err);
|
|
3887
3985
|
const error = err instanceof Error ? err : new Error(String(err));
|
|
3888
3986
|
if (
|
|
3889
|
-
await
|
|
3987
|
+
await recoverAcceptedRecordingAfterFinalizeError({
|
|
3890
3988
|
serverUrl: params.serverUrl,
|
|
3891
3989
|
recordingId: id,
|
|
3892
3990
|
authToken: params.authToken,
|
|
@@ -2,6 +2,7 @@ export interface FinalizeReceipt {
|
|
|
2
2
|
ok?: unknown;
|
|
3
3
|
finalized?: unknown;
|
|
4
4
|
status?: unknown;
|
|
5
|
+
verificationPending?: unknown;
|
|
5
6
|
sourceSizeBytes?: unknown;
|
|
6
7
|
durationMs?: unknown;
|
|
7
8
|
}
|
|
@@ -11,16 +12,22 @@ export interface LocalRecordingProof {
|
|
|
11
12
|
durationMs: number;
|
|
12
13
|
}
|
|
13
14
|
|
|
15
|
+
export type FinalizeReceiptStatus = "processing" | "ready";
|
|
16
|
+
|
|
14
17
|
export function verifyFinalizeReceipt(
|
|
15
18
|
receipt: FinalizeReceipt | null,
|
|
16
19
|
local: LocalRecordingProof,
|
|
17
|
-
):
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
receipt.
|
|
21
|
-
receipt.
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
): FinalizeReceiptStatus {
|
|
21
|
+
const ready =
|
|
22
|
+
receipt?.ok === true &&
|
|
23
|
+
receipt.finalized === true &&
|
|
24
|
+
receipt.status === "ready";
|
|
25
|
+
const processing =
|
|
26
|
+
receipt?.ok === true &&
|
|
27
|
+
receipt.finalized === false &&
|
|
28
|
+
receipt.status === "processing" &&
|
|
29
|
+
receipt.verificationPending === true;
|
|
30
|
+
if (!ready && !processing) {
|
|
24
31
|
throw new Error(
|
|
25
32
|
"Clip may be incomplete. Finalization was not confirmed; the local backup was kept.",
|
|
26
33
|
);
|
|
@@ -48,4 +55,6 @@ export function verifyFinalizeReceipt(
|
|
|
48
55
|
"Clip may be incomplete. The uploaded duration did not match the local recording; the local backup was kept.",
|
|
49
56
|
);
|
|
50
57
|
}
|
|
58
|
+
|
|
59
|
+
return ready ? "ready" : "processing";
|
|
51
60
|
}
|
|
@@ -249,8 +249,7 @@ pub fn run() {
|
|
|
249
249
|
use tauri_plugin_deep_link::DeepLinkExt;
|
|
250
250
|
let dl_handle = app.handle().clone();
|
|
251
251
|
app.deep_link().on_open_url(move |event| {
|
|
252
|
-
let urls: Vec<String> =
|
|
253
|
-
event.urls().iter().map(|u| u.to_string()).collect();
|
|
252
|
+
let urls: Vec<String> = event.urls().iter().map(|u| u.to_string()).collect();
|
|
254
253
|
dlog!("[clips-tray] deep link opened: {:?}", urls);
|
|
255
254
|
present_popover(&dl_handle);
|
|
256
255
|
let _ = dl_handle.emit("clips:deep-link", urls);
|
|
@@ -263,8 +262,7 @@ pub fn run() {
|
|
|
263
262
|
// arguments rather than an on_open_url event. Consume it here so
|
|
264
263
|
// the popover appears and the browser does not hit its fallback.
|
|
265
264
|
if let Ok(Some(urls)) = app.deep_link().get_current() {
|
|
266
|
-
let url_strings: Vec<String> =
|
|
267
|
-
urls.iter().map(|u| u.to_string()).collect();
|
|
265
|
+
let url_strings: Vec<String> = urls.iter().map(|u| u.to_string()).collect();
|
|
268
266
|
dlog!("[clips-tray] deep link cold start: {:?}", url_strings);
|
|
269
267
|
present_popover(app.handle());
|
|
270
268
|
let _ = app.handle().emit("clips:deep-link", url_strings);
|