@agent-native/core 0.109.0 → 0.109.2

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 (154) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +14 -0
  3. package/corpus/core/docs/content/integrations.mdx +87 -253
  4. package/corpus/core/package.json +1 -1
  5. package/corpus/core/src/a2a/client.ts +24 -2
  6. package/corpus/core/src/a2a/correlation.ts +50 -0
  7. package/corpus/core/src/a2a/handlers.ts +140 -14
  8. package/corpus/core/src/a2a/index.ts +1 -0
  9. package/corpus/core/src/a2a/invoke.ts +10 -1
  10. package/corpus/core/src/a2a/task-store.ts +146 -6
  11. package/corpus/core/src/a2a/types.ts +16 -0
  12. package/corpus/core/src/action.ts +2 -0
  13. package/corpus/core/src/agent/production-agent.ts +23 -0
  14. package/corpus/core/src/client/blocks/library/diagram.config.ts +10 -1
  15. package/corpus/core/src/client/blocks/library/diagram.tsx +9 -4
  16. package/corpus/core/src/client/use-run-stuck-detection.ts +27 -1
  17. package/corpus/core/src/db/client.ts +55 -0
  18. package/corpus/core/src/deploy/build.ts +63 -3
  19. package/corpus/core/src/observability/traces.ts +172 -19
  20. package/corpus/core/src/scripts/call-agent.ts +53 -2
  21. package/corpus/core/src/server/agent-chat/action-filters-a2a.ts +72 -25
  22. package/corpus/core/src/server/agent-chat-plugin.ts +107 -6
  23. package/corpus/core/src/templates/workspace-core/.agents/skills/a2a-protocol/SKILL.md +21 -0
  24. package/corpus/core/src/templates/workspace-core/.agents/skills/observability/SKILL.md +7 -1
  25. package/corpus/core/src/templates/workspace-core/.agents/skills/tracking/SKILL.md +1 -1
  26. package/corpus/templates/analytics/actions/query-agent-native-analytics.ts +1 -1
  27. package/corpus/templates/analytics/changelog/2026-07-17-analytics-now-includes-a-default-alert-for-spikes-in-stuck-agent-chats.md +6 -0
  28. package/corpus/templates/analytics/docs/schemas/first-party-analytics.md +23 -14
  29. package/corpus/templates/analytics/docs/schemas/tracked-events.md +5 -1
  30. package/corpus/templates/analytics/server/jobs/analytics-alerts.ts +3 -3
  31. package/corpus/templates/analytics/server/lib/analytics-alerts.ts +209 -87
  32. package/corpus/templates/clips/.agents/skills/dictate/SKILL.md +28 -5
  33. package/corpus/templates/clips/.agents/skills/meetings/SKILL.md +7 -0
  34. package/corpus/templates/clips/.agents/skills/recording/SKILL.md +23 -0
  35. package/corpus/templates/clips/AGENTS.md +10 -0
  36. package/corpus/templates/clips/actions/create-dictation.ts +29 -4
  37. package/corpus/templates/clips/actions/finalize-recording.ts +542 -65
  38. package/corpus/templates/clips/actions/get-recording-player-data.ts +16 -6
  39. package/corpus/templates/clips/actions/set-mobile-capture-state.ts +37 -0
  40. package/corpus/templates/clips/actions/update-dictation.ts +9 -1
  41. package/corpus/templates/clips/app/components/player/playback-comment-overlay.tsx +17 -2
  42. package/corpus/templates/clips/app/components/player/video-player.tsx +14 -2
  43. package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +2 -2
  44. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +14 -3
  45. package/corpus/templates/clips/app/routes/record.tsx +3 -3
  46. package/corpus/templates/clips/app/routes/share.$shareId.tsx +19 -5
  47. package/corpus/templates/clips/changelog/2026-07-17-capture-dictation-background-meeting-audio-and-camera-videos.md +6 -0
  48. package/corpus/templates/clips/changelog/2026-07-17-clips-now-retries-storage-verification-in-the-background-ins.md +6 -0
  49. package/corpus/templates/clips/changelog/2026-07-17-delayed-upload-verification-keeps-local-backups-retryable.md +6 -0
  50. package/corpus/templates/clips/changelog/2026-07-17-fixed-broken-thumbnail-icons-appearing-over-playable-clip-em.md +6 -0
  51. package/corpus/templates/clips/changelog/2026-07-17-playback-comments-stay-readable-at-faster-playback-speeds.md +6 -0
  52. package/corpus/templates/clips/chrome-extension/src/finalize-recovery.ts +3 -0
  53. package/corpus/templates/clips/chrome-extension/src/offscreen.ts +3 -3
  54. package/corpus/templates/clips/desktop/src/app.tsx +35 -6
  55. package/corpus/templates/clips/desktop/src/lib/processing-backup-recovery.ts +19 -0
  56. package/corpus/templates/clips/desktop/src/lib/recorder.ts +141 -18
  57. package/corpus/templates/clips/desktop/src/lib/upload-verification.ts +28 -7
  58. package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +3 -4
  59. package/corpus/templates/clips/desktop/src-tauri/src/native_screen/live_upload.rs +32 -15
  60. package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +59 -18
  61. package/corpus/templates/clips/server/db/schema.ts +9 -1
  62. package/corpus/templates/clips/server/jobs/media-verification.ts +130 -0
  63. package/corpus/templates/clips/server/lib/builder-media-compression.ts +74 -7
  64. package/corpus/templates/clips/server/lib/media-verification-state.ts +55 -0
  65. package/corpus/templates/clips/server/lib/player-thumbnail-url.ts +31 -0
  66. package/corpus/templates/clips/server/lib/post-finalize-dispatch.ts +10 -3
  67. package/corpus/templates/clips/server/plugins/auth.ts +5 -5
  68. package/corpus/templates/clips/server/plugins/calendar-jobs.ts +2 -0
  69. package/corpus/templates/clips/server/routes/api/_agent-native-background/post-finalize-worker.post.ts +12 -2
  70. package/corpus/templates/clips/server/routes/api/public-recording.get.ts +13 -1
  71. package/corpus/templates/clips/server/routes/api/thumbnail/[recordingId].get.ts +340 -0
  72. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/abort.post.ts +24 -7
  73. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/chunk.post.ts +91 -7
  74. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/reset-chunks.post.ts +22 -1
  75. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/status.get.ts +14 -0
  76. package/corpus/templates/clips/shared/finalize-recovery.ts +61 -2
  77. package/corpus/templates/clips/shared/share-meta.ts +1 -1
  78. package/corpus/templates/plan/changelog/2026-07-17-diagrams-can-opt-into-a-polished-clean-rendering-mode-withou.md +6 -0
  79. package/corpus/templates/plan/shared/plan-content.ts +3 -0
  80. package/dist/a2a/client.d.ts +11 -2
  81. package/dist/a2a/client.d.ts.map +1 -1
  82. package/dist/a2a/client.js +16 -3
  83. package/dist/a2a/client.js.map +1 -1
  84. package/dist/a2a/correlation.d.ts +10 -0
  85. package/dist/a2a/correlation.d.ts.map +1 -0
  86. package/dist/a2a/correlation.js +40 -0
  87. package/dist/a2a/correlation.js.map +1 -0
  88. package/dist/a2a/handlers.d.ts.map +1 -1
  89. package/dist/a2a/handlers.js +100 -15
  90. package/dist/a2a/handlers.js.map +1 -1
  91. package/dist/a2a/index.d.ts +1 -1
  92. package/dist/a2a/index.d.ts.map +1 -1
  93. package/dist/a2a/index.js.map +1 -1
  94. package/dist/a2a/invoke.d.ts +4 -1
  95. package/dist/a2a/invoke.d.ts.map +1 -1
  96. package/dist/a2a/invoke.js +3 -0
  97. package/dist/a2a/invoke.js.map +1 -1
  98. package/dist/a2a/task-store.d.ts +12 -1
  99. package/dist/a2a/task-store.d.ts.map +1 -1
  100. package/dist/a2a/task-store.js +96 -6
  101. package/dist/a2a/task-store.js.map +1 -1
  102. package/dist/a2a/types.d.ts +15 -0
  103. package/dist/a2a/types.d.ts.map +1 -1
  104. package/dist/a2a/types.js.map +1 -1
  105. package/dist/action.d.ts +2 -0
  106. package/dist/action.d.ts.map +1 -1
  107. package/dist/action.js.map +1 -1
  108. package/dist/agent/production-agent.d.ts +9 -0
  109. package/dist/agent/production-agent.d.ts.map +1 -1
  110. package/dist/agent/production-agent.js +14 -0
  111. package/dist/agent/production-agent.js.map +1 -1
  112. package/dist/client/blocks/library/diagram.config.d.ts +2 -0
  113. package/dist/client/blocks/library/diagram.config.d.ts.map +1 -1
  114. package/dist/client/blocks/library/diagram.config.js +8 -1
  115. package/dist/client/blocks/library/diagram.config.js.map +1 -1
  116. package/dist/client/blocks/library/diagram.d.ts.map +1 -1
  117. package/dist/client/blocks/library/diagram.js +9 -4
  118. package/dist/client/blocks/library/diagram.js.map +1 -1
  119. package/dist/client/use-run-stuck-detection.d.ts.map +1 -1
  120. package/dist/client/use-run-stuck-detection.js +25 -1
  121. package/dist/client/use-run-stuck-detection.js.map +1 -1
  122. package/dist/collab/routes.d.ts +1 -1
  123. package/dist/db/client.d.ts +7 -0
  124. package/dist/db/client.d.ts.map +1 -1
  125. package/dist/db/client.js +33 -0
  126. package/dist/db/client.js.map +1 -1
  127. package/dist/deploy/build.d.ts +3 -0
  128. package/dist/deploy/build.d.ts.map +1 -1
  129. package/dist/deploy/build.js +45 -1
  130. package/dist/deploy/build.js.map +1 -1
  131. package/dist/observability/routes.d.ts +1 -1
  132. package/dist/observability/traces.d.ts +9 -0
  133. package/dist/observability/traces.d.ts.map +1 -1
  134. package/dist/observability/traces.js +137 -19
  135. package/dist/observability/traces.js.map +1 -1
  136. package/dist/progress/routes.d.ts +1 -1
  137. package/dist/scripts/call-agent.d.ts.map +1 -1
  138. package/dist/scripts/call-agent.js +35 -3
  139. package/dist/scripts/call-agent.js.map +1 -1
  140. package/dist/server/agent-chat/action-filters-a2a.d.ts +18 -2
  141. package/dist/server/agent-chat/action-filters-a2a.d.ts.map +1 -1
  142. package/dist/server/agent-chat/action-filters-a2a.js +35 -7
  143. package/dist/server/agent-chat/action-filters-a2a.js.map +1 -1
  144. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  145. package/dist/server/agent-chat-plugin.js +93 -6
  146. package/dist/server/agent-chat-plugin.js.map +1 -1
  147. package/dist/templates/workspace-core/.agents/skills/a2a-protocol/SKILL.md +21 -0
  148. package/dist/templates/workspace-core/.agents/skills/observability/SKILL.md +7 -1
  149. package/dist/templates/workspace-core/.agents/skills/tracking/SKILL.md +1 -1
  150. package/docs/content/integrations.mdx +87 -253
  151. package/package.json +1 -1
  152. package/src/templates/workspace-core/.agents/skills/a2a-protocol/SKILL.md +21 -0
  153. package/src/templates/workspace-core/.agents/skills/observability/SKILL.md +7 -1
  154. package/src/templates/workspace-core/.agents/skills/tracking/SKILL.md +1 -1
@@ -29,6 +29,7 @@ import { and, asc, eq, or, sql } from "drizzle-orm";
29
29
  import { z } from "zod";
30
30
 
31
31
  import { getDb, schema } from "../server/db/index.js";
32
+ import { isMediaVerificationPending } from "../server/lib/media-verification-state.js";
32
33
  import { resolvePlayerVideoUrl } from "../server/lib/player-video-url.js";
33
34
  import {
34
35
  canOpenDirectRecordingPage,
@@ -178,12 +179,20 @@ export default defineAction({
178
179
  access.role === "owner" ||
179
180
  access.role === "admin" ||
180
181
  access.role === "editor";
181
- const [cleanupStateRaw, builderCreditsRaw] = await Promise.all([
182
- readAppState(`transcript-cleanup-${args.recordingId}`).catch(() => null),
183
- canEditRecording
184
- ? readAppState(CLIPS_BUILDER_CREDITS_STATE_KEY).catch(() => null)
185
- : Promise.resolve(null),
186
- ]);
182
+ const [cleanupStateRaw, builderCreditsRaw, verificationPending] =
183
+ await Promise.all([
184
+ readAppState(`transcript-cleanup-${args.recordingId}`).catch(
185
+ () => null,
186
+ ),
187
+ canEditRecording
188
+ ? readAppState(CLIPS_BUILDER_CREDITS_STATE_KEY).catch(() => null)
189
+ : Promise.resolve(null),
190
+ isMediaVerificationPending({
191
+ ownerEmail: rec.ownerEmail,
192
+ recordingId: args.recordingId,
193
+ recordingStatus: rec.status,
194
+ }),
195
+ ]);
187
196
  const cleanupState =
188
197
  cleanupStateRaw && typeof cleanupStateRaw === "object"
189
198
  ? (cleanupStateRaw as Record<string, unknown>)
@@ -326,6 +335,7 @@ export default defineAction({
326
335
  hasAudio: Boolean(rec.hasAudio),
327
336
  hasCamera: Boolean(rec.hasCamera),
328
337
  status: rec.status,
338
+ verificationPending,
329
339
  uploadProgress: rec.uploadProgress,
330
340
  failureReason: rec.failureReason,
331
341
  // Don't leak the password to clients (especially to MCP hosts that
@@ -0,0 +1,37 @@
1
+ import { defineAction } from "@agent-native/core/action";
2
+ import { writeAppState } from "@agent-native/core/application-state";
3
+ import { z } from "zod";
4
+
5
+ export const MOBILE_CAPTURE_STATE_KEY = "mobile-capture-state";
6
+
7
+ const mobileCaptureStateSchema = z
8
+ .object({
9
+ view: z.enum(["home", "dictate", "meeting", "video"]),
10
+ phase: z.enum([
11
+ "idle",
12
+ "ready",
13
+ "recording",
14
+ "paused",
15
+ "saving",
16
+ "processing",
17
+ "review",
18
+ "error",
19
+ ]),
20
+ captureId: z.string().trim().min(1).max(200).optional(),
21
+ })
22
+ .strict();
23
+
24
+ export default defineAction({
25
+ description:
26
+ "Record the privacy-safe view and phase of the Agent Native mobile capture companion.",
27
+ agentTool: false,
28
+ schema: mobileCaptureStateSchema,
29
+ run: async (args) => {
30
+ const state = {
31
+ ...args,
32
+ updatedAt: new Date().toISOString(),
33
+ };
34
+ await writeAppState(MOBILE_CAPTURE_STATE_KEY, state);
35
+ return state;
36
+ },
37
+ });
@@ -20,7 +20,15 @@ export default defineAction({
20
20
  fullText: z.string().optional(),
21
21
  cleanedText: z.string().nullable().optional(),
22
22
  source: z
23
- .enum(["fn-hold", "cmd-shift-space", "manual", "other", "fn", "custom"])
23
+ .enum([
24
+ "fn-hold",
25
+ "cmd-shift-space",
26
+ "manual",
27
+ "mobile",
28
+ "other",
29
+ "fn",
30
+ "custom",
31
+ ])
24
32
  .optional(),
25
33
  targetApp: z.string().nullable().optional(),
26
34
  }),
@@ -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
 
@@ -376,6 +376,7 @@ export default function RecordingPage() {
376
376
  }, [recordingId, playerDataForbidden, navigate]);
377
377
 
378
378
  const recording = playerDataQ.data?.recording;
379
+ const verificationPending = recording?.verificationPending === true;
379
380
  const role = playerDataQ.data?.role as
380
381
  | "owner"
381
382
  | "admin"
@@ -781,13 +782,22 @@ export default function RecordingPage() {
781
782
  setProcessingTimeout(false);
782
783
  return;
783
784
  }
785
+ if (verificationPending) {
786
+ setProcessingTimeout(false);
787
+ return;
788
+ }
784
789
  const timeoutMs =
785
790
  recording.status === "processing"
786
791
  ? PROCESSING_STUCK_TIMEOUT_MS
787
792
  : UPLOAD_STUCK_TIMEOUT_MS;
788
793
  const handle = setTimeout(() => setProcessingTimeout(true), timeoutMs);
789
794
  return () => clearTimeout(handle);
790
- }, [recording?.status, recording?.videoUrl, recordingId]);
795
+ }, [
796
+ recording?.status,
797
+ recording?.videoUrl,
798
+ recordingId,
799
+ verificationPending,
800
+ ]);
791
801
 
792
802
  usePlayerShortcuts({ playerRef, chapters });
793
803
 
@@ -873,7 +883,8 @@ export default function RecordingPage() {
873
883
  isNativeSaveFailureReason(rawFailureReason);
874
884
  // Give a long-running desktop save an actionable recovery state without
875
885
  // claiming the upload failed while its bounded final request is still live.
876
- const stuckFailure = !explicitFailure && processingTimeout;
886
+ const stuckFailure =
887
+ !explicitFailure && !verificationPending && processingTimeout;
877
888
  const isFailure = explicitFailure || waitingForStorage || nativeSaveFailed;
878
889
  const showRecoveryState = isFailure || stuckFailure;
879
890
  const displayReason = explicitFailure
@@ -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
 
@@ -475,6 +478,7 @@ export default function ShareRoute() {
475
478
  });
476
479
 
477
480
  const recording = dataQ.data?.data?.recording;
481
+ const verificationPending = recording?.verificationPending === true;
478
482
  const comments = dataQ.data?.data?.comments ?? [];
479
483
  const reactions = dataQ.data?.data?.reactions ?? [];
480
484
  const chapters = dataQ.data?.data?.chapters ?? [];
@@ -557,6 +561,10 @@ export default function ShareRoute() {
557
561
  setProcessingTimeout(false);
558
562
  return;
559
563
  }
564
+ if (verificationPending) {
565
+ setProcessingTimeout(false);
566
+ return;
567
+ }
560
568
 
561
569
  const timeoutMs =
562
570
  recording.status === "processing"
@@ -564,7 +572,12 @@ export default function ShareRoute() {
564
572
  : UPLOAD_STUCK_TIMEOUT_MS;
565
573
  const handle = setTimeout(() => setProcessingTimeout(true), timeoutMs);
566
574
  return () => clearTimeout(handle);
567
- }, [recording?.id, recording?.status, recording?.videoUrl]);
575
+ }, [
576
+ recording?.id,
577
+ recording?.status,
578
+ recording?.videoUrl,
579
+ verificationPending,
580
+ ]);
568
581
 
569
582
  usePlayerShortcuts({ playerRef });
570
583
 
@@ -709,7 +722,8 @@ export default function ShareRoute() {
709
722
  const storageSetupFailure = isStorageSetupFailureReason(rawFailureReason);
710
723
  const loomStorageSetupFailure =
711
724
  storageSetupFailure && isLoomRecordingSource(recording);
712
- const stuckFailure = !explicitFailure && processingTimeout;
725
+ const stuckFailure =
726
+ !explicitFailure && !verificationPending && processingTimeout;
713
727
  const isFailure = explicitFailure || storageSetupFailure || stuckFailure;
714
728
  const canManageStorage = viewerCanEdit;
715
729
  const signInHref = buildSignInHref(`/r/${recording.id}`);
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: added
3
+ date: 2026-07-17
4
+ ---
5
+
6
+ Capture dictation, background meeting audio, and camera videos from the Agent Native iOS and Android app with automatic recovery and resumable Clips sync.
@@ -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
+ Delayed upload verification now keeps local backups retryable and avoids showing a false processing failure.
@@ -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,
@@ -1834,6 +1835,23 @@ export function App() {
1834
1835
  );
1835
1836
  }, []);
1836
1837
 
1838
+ useEffect(() => {
1839
+ let unlisten: (() => void) | null = null;
1840
+ let cancelled = false;
1841
+ listen("clips:pending-uploads-changed", () => {
1842
+ void loadPendingUploads();
1843
+ })
1844
+ .then((stop) => {
1845
+ if (cancelled) stop();
1846
+ else unlisten = stop;
1847
+ })
1848
+ .catch(() => {});
1849
+ return () => {
1850
+ cancelled = true;
1851
+ unlisten?.();
1852
+ };
1853
+ }, [loadPendingUploads]);
1854
+
1837
1855
  useEffect(() => {
1838
1856
  if (popoverView === "meetings" && popoverVisible) {
1839
1857
  void fetchUpcomingMeetings();
@@ -1894,12 +1912,23 @@ export function App() {
1894
1912
  try {
1895
1913
  const authToken = loadDesktopAuthToken(targetServerUrl);
1896
1914
  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
- });
1915
+ const result = await invoke<{ verificationPending?: boolean }>(
1916
+ "native_fullscreen_recording_retry_upload",
1917
+ {
1918
+ serverUrl: targetServerUrl,
1919
+ recordingId: upload.recordingId,
1920
+ authToken,
1921
+ cookie:
1922
+ typeof document !== "undefined" ? document.cookie || "" : "",
1923
+ },
1924
+ );
1925
+ if (result.verificationPending) {
1926
+ scheduleNativeBackupCleanupAfterProcessing({
1927
+ serverUrl: targetServerUrl,
1928
+ recordingId: upload.recordingId,
1929
+ authToken,
1930
+ });
1931
+ }
1903
1932
  } else {
1904
1933
  await retryBrowserRecordingBackup({
1905
1934
  recordingId: upload.recordingId,
@@ -0,0 +1,19 @@
1
+ export async function reconcileProcessingBackup(args: {
2
+ waitForReady: () => Promise<{ status?: string } | null>;
3
+ onReady: () => Promise<unknown>;
4
+ onUnresolved: () => Promise<unknown>;
5
+ onPollError?: (error: unknown) => void;
6
+ }): Promise<"ready" | "unresolved"> {
7
+ try {
8
+ const recovered = await args.waitForReady();
9
+ if (recovered?.status === "ready") {
10
+ await args.onReady();
11
+ return "ready";
12
+ }
13
+ } catch (error) {
14
+ args.onPollError?.(error);
15
+ }
16
+
17
+ await args.onUnresolved();
18
+ return "unresolved";
19
+ }