@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.
Files changed (125) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +7 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/a2a/client.ts +24 -2
  5. package/corpus/core/src/a2a/correlation.ts +50 -0
  6. package/corpus/core/src/a2a/handlers.ts +140 -14
  7. package/corpus/core/src/a2a/index.ts +1 -0
  8. package/corpus/core/src/a2a/invoke.ts +10 -1
  9. package/corpus/core/src/a2a/task-store.ts +146 -6
  10. package/corpus/core/src/a2a/types.ts +16 -0
  11. package/corpus/core/src/action.ts +2 -0
  12. package/corpus/core/src/agent/production-agent.ts +23 -0
  13. package/corpus/core/src/client/use-run-stuck-detection.ts +27 -1
  14. package/corpus/core/src/db/client.ts +55 -0
  15. package/corpus/core/src/observability/traces.ts +172 -19
  16. package/corpus/core/src/scripts/call-agent.ts +53 -2
  17. package/corpus/core/src/server/agent-chat/action-filters-a2a.ts +72 -25
  18. package/corpus/core/src/server/agent-chat-plugin.ts +107 -6
  19. package/corpus/core/src/templates/workspace-core/.agents/skills/a2a-protocol/SKILL.md +21 -0
  20. package/corpus/core/src/templates/workspace-core/.agents/skills/observability/SKILL.md +7 -1
  21. package/corpus/core/src/templates/workspace-core/.agents/skills/tracking/SKILL.md +1 -1
  22. package/corpus/templates/analytics/actions/query-agent-native-analytics.ts +1 -1
  23. package/corpus/templates/analytics/changelog/2026-07-17-analytics-now-includes-a-default-alert-for-spikes-in-stuck-agent-chats.md +6 -0
  24. package/corpus/templates/analytics/docs/schemas/first-party-analytics.md +23 -14
  25. package/corpus/templates/analytics/docs/schemas/tracked-events.md +5 -1
  26. package/corpus/templates/analytics/server/jobs/analytics-alerts.ts +3 -3
  27. package/corpus/templates/analytics/server/lib/analytics-alerts.ts +209 -87
  28. package/corpus/templates/clips/actions/finalize-recording.ts +541 -62
  29. package/corpus/templates/clips/app/components/player/playback-comment-overlay.tsx +17 -2
  30. package/corpus/templates/clips/app/components/player/video-player.tsx +14 -2
  31. package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +2 -2
  32. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +1 -1
  33. package/corpus/templates/clips/app/routes/record.tsx +3 -3
  34. package/corpus/templates/clips/app/routes/share.$shareId.tsx +6 -3
  35. package/corpus/templates/clips/changelog/2026-07-17-clips-now-retries-storage-verification-in-the-background-ins.md +6 -0
  36. package/corpus/templates/clips/changelog/2026-07-17-fixed-broken-thumbnail-icons-appearing-over-playable-clip-em.md +6 -0
  37. package/corpus/templates/clips/changelog/2026-07-17-playback-comments-stay-readable-at-faster-playback-speeds.md +6 -0
  38. package/corpus/templates/clips/chrome-extension/src/finalize-recovery.ts +3 -0
  39. package/corpus/templates/clips/chrome-extension/src/offscreen.ts +3 -3
  40. package/corpus/templates/clips/desktop/src/app.tsx +18 -6
  41. package/corpus/templates/clips/desktop/src/lib/recorder.ts +111 -13
  42. package/corpus/templates/clips/desktop/src/lib/upload-verification.ts +16 -7
  43. package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +2 -4
  44. package/corpus/templates/clips/desktop/src-tauri/src/native_screen/live_upload.rs +32 -15
  45. package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +45 -18
  46. package/corpus/templates/clips/server/jobs/media-verification.ts +130 -0
  47. package/corpus/templates/clips/server/lib/builder-media-compression.ts +15 -0
  48. package/corpus/templates/clips/server/lib/media-verification-state.ts +38 -0
  49. package/corpus/templates/clips/server/lib/player-thumbnail-url.ts +31 -0
  50. package/corpus/templates/clips/server/lib/post-finalize-dispatch.ts +10 -3
  51. package/corpus/templates/clips/server/plugins/auth.ts +5 -5
  52. package/corpus/templates/clips/server/plugins/calendar-jobs.ts +2 -0
  53. package/corpus/templates/clips/server/routes/api/_agent-native-background/post-finalize-worker.post.ts +12 -2
  54. package/corpus/templates/clips/server/routes/api/public-recording.get.ts +6 -1
  55. package/corpus/templates/clips/server/routes/api/thumbnail/[recordingId].get.ts +340 -0
  56. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/abort.post.ts +24 -7
  57. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/chunk.post.ts +91 -7
  58. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/status.get.ts +14 -0
  59. package/corpus/templates/clips/shared/finalize-recovery.ts +61 -2
  60. package/corpus/templates/clips/shared/share-meta.ts +1 -1
  61. package/dist/a2a/client.d.ts +11 -2
  62. package/dist/a2a/client.d.ts.map +1 -1
  63. package/dist/a2a/client.js +16 -3
  64. package/dist/a2a/client.js.map +1 -1
  65. package/dist/a2a/correlation.d.ts +10 -0
  66. package/dist/a2a/correlation.d.ts.map +1 -0
  67. package/dist/a2a/correlation.js +40 -0
  68. package/dist/a2a/correlation.js.map +1 -0
  69. package/dist/a2a/handlers.d.ts.map +1 -1
  70. package/dist/a2a/handlers.js +100 -15
  71. package/dist/a2a/handlers.js.map +1 -1
  72. package/dist/a2a/index.d.ts +1 -1
  73. package/dist/a2a/index.d.ts.map +1 -1
  74. package/dist/a2a/index.js.map +1 -1
  75. package/dist/a2a/invoke.d.ts +4 -1
  76. package/dist/a2a/invoke.d.ts.map +1 -1
  77. package/dist/a2a/invoke.js +3 -0
  78. package/dist/a2a/invoke.js.map +1 -1
  79. package/dist/a2a/task-store.d.ts +12 -1
  80. package/dist/a2a/task-store.d.ts.map +1 -1
  81. package/dist/a2a/task-store.js +96 -6
  82. package/dist/a2a/task-store.js.map +1 -1
  83. package/dist/a2a/types.d.ts +15 -0
  84. package/dist/a2a/types.d.ts.map +1 -1
  85. package/dist/a2a/types.js.map +1 -1
  86. package/dist/action.d.ts +2 -0
  87. package/dist/action.d.ts.map +1 -1
  88. package/dist/action.js.map +1 -1
  89. package/dist/agent/production-agent.d.ts +9 -0
  90. package/dist/agent/production-agent.d.ts.map +1 -1
  91. package/dist/agent/production-agent.js +14 -0
  92. package/dist/agent/production-agent.js.map +1 -1
  93. package/dist/client/use-run-stuck-detection.d.ts.map +1 -1
  94. package/dist/client/use-run-stuck-detection.js +25 -1
  95. package/dist/client/use-run-stuck-detection.js.map +1 -1
  96. package/dist/collab/routes.d.ts +1 -1
  97. package/dist/collab/struct-routes.d.ts +1 -1
  98. package/dist/db/client.d.ts +7 -0
  99. package/dist/db/client.d.ts.map +1 -1
  100. package/dist/db/client.js +33 -0
  101. package/dist/db/client.js.map +1 -1
  102. package/dist/observability/routes.d.ts +5 -5
  103. package/dist/observability/traces.d.ts +9 -0
  104. package/dist/observability/traces.d.ts.map +1 -1
  105. package/dist/observability/traces.js +137 -19
  106. package/dist/observability/traces.js.map +1 -1
  107. package/dist/resources/handlers.d.ts +2 -2
  108. package/dist/scripts/call-agent.d.ts.map +1 -1
  109. package/dist/scripts/call-agent.js +35 -3
  110. package/dist/scripts/call-agent.js.map +1 -1
  111. package/dist/secrets/routes.d.ts +9 -9
  112. package/dist/server/agent-chat/action-filters-a2a.d.ts +18 -2
  113. package/dist/server/agent-chat/action-filters-a2a.d.ts.map +1 -1
  114. package/dist/server/agent-chat/action-filters-a2a.js +35 -7
  115. package/dist/server/agent-chat/action-filters-a2a.js.map +1 -1
  116. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  117. package/dist/server/agent-chat-plugin.js +93 -6
  118. package/dist/server/agent-chat-plugin.js.map +1 -1
  119. package/dist/templates/workspace-core/.agents/skills/a2a-protocol/SKILL.md +21 -0
  120. package/dist/templates/workspace-core/.agents/skills/observability/SKILL.md +7 -1
  121. package/dist/templates/workspace-core/.agents/skills/tracking/SKILL.md +1 -1
  122. package/package.json +1 -1
  123. package/src/templates/workspace-core/.agents/skills/a2a-protocol/SKILL.md +21 -0
  124. package/src/templates/workspace-core/.agents/skills/observability/SKILL.md +7 -1
  125. 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 + PLAYBACK_COMMENT_VISIBLE_MS
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(comments, currentMs);
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 comments={comments} currentMs={currentMs} />
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 { waitForReadyRecordingAfterFinalizeError } from "@shared/finalize-recovery";
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 waitForReadyRecordingAfterFinalizeError({
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 = 2 * 60 * 1000;
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
- waitForReadyRecordingAfterFinalizeError,
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 waitForReadyRecordingAfterFinalizeError({
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 waitForReadyRecordingAfterFinalizeError({
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.thumbnailUrl,
230
- animatedThumbnailUrl: rec.animatedThumbnailUrl,
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 = 2 * 60 * 1000;
292
+ const PROCESSING_STUCK_TIMEOUT_MS = 12 * 60 * 1000;
290
293
 
291
294
  type ViewerPlatform = "mac" | "windows" | "linux";
292
295
 
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-07-17
4
+ ---
5
+
6
+ Clips now retries storage verification in the background instead of failing completed uploads prematurely.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-07-17
4
+ ---
5
+
6
+ Fixed broken thumbnail icons appearing over playable clip embeds
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-07-17
4
+ ---
5
+
6
+ Playback comments stay readable at faster playback speeds
@@ -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 { waitForReadyRecordingAfterFinalizeError } from "./finalize-recovery";
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 waitForReadyRecordingAfterFinalizeError({
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 waitForReadyRecordingAfterFinalizeError({
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("native_fullscreen_recording_retry_upload", {
1898
- serverUrl: targetServerUrl,
1899
- recordingId: upload.recordingId,
1900
- authToken,
1901
- cookie: typeof document !== "undefined" ? document.cookie || "" : "",
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 { waitForReadyRecordingAfterFinalizeError } from "../../../shared/finalize-recovery";
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
- async function recoverReadyRecordingAfterFinalizeError({
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<boolean> {
764
- const recovered = await waitForReadyRecordingAfterFinalizeError({
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 false;
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 true;
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 recoverReadyRecordingAfterFinalizeError({
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 recoverReadyRecordingAfterFinalizeError({
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 recoverReadyRecordingAfterFinalizeError({
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 recoverReadyRecordingAfterFinalizeError({
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
- ): void {
18
- if (
19
- !receipt ||
20
- receipt.ok !== true ||
21
- receipt.finalized !== true ||
22
- receipt.status !== "ready"
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);