@agent-native/core 0.155.0 → 0.157.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 (192) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/analytics/server/jobs/analytics-bigquery-backfill.ts +19 -4
  3. package/corpus/templates/analytics/server/lib/first-party-analytics-backend.ts +7 -2
  4. package/corpus/templates/assets/app/components/asset/AssetPreviewDialog.tsx +3 -1
  5. package/corpus/templates/assets/app/global.css +4 -0
  6. package/corpus/templates/assets/app/i18n-data.ts +21 -1
  7. package/corpus/templates/assets/app/routes/brand-kits.$id.tsx +18 -15
  8. package/corpus/templates/brain/app/routes/sources.tsx +1 -2
  9. package/corpus/templates/calendar/app/hooks/use-bookings.ts +12 -2
  10. package/corpus/templates/calendar/app/pages/BookingLinksPage.tsx +64 -2
  11. package/corpus/templates/calendar/server/handlers/bookings.ts +219 -93
  12. package/corpus/templates/calendar/server/plugins/auth.ts +0 -2
  13. package/corpus/templates/clips/actions/create-folder.ts +7 -1
  14. package/corpus/templates/clips/actions/create-meeting.ts +6 -2
  15. package/corpus/templates/clips/actions/create-recording.ts +5 -3
  16. package/corpus/templates/clips/actions/finalize-recording.ts +97 -65
  17. package/corpus/templates/clips/actions/import-loom-recording.ts +5 -3
  18. package/corpus/templates/clips/actions/request-recording-access.ts +272 -0
  19. package/corpus/templates/clips/actions/stitch-recordings.ts +5 -3
  20. package/corpus/templates/clips/app/components/meetings/share-meeting-dialog.tsx +38 -63
  21. package/corpus/templates/clips/app/components/player/share-dialog.tsx +177 -134
  22. package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +3 -21
  23. package/corpus/templates/clips/app/components/sharing/share-ui.tsx +13 -30
  24. package/corpus/templates/clips/app/i18n/en-US.ts +10 -0
  25. package/corpus/templates/clips/app/routes/_app.meetings.$meetingId.tsx +5 -5
  26. package/corpus/templates/clips/app/routes/r.$recordingId.tsx +24 -17
  27. package/corpus/templates/clips/app/routes/share.$shareId.tsx +151 -20
  28. package/corpus/templates/clips/changelog/2026-08-14-long-resumable-uploads-now-abort-cleanly-and-show-errors.md +6 -0
  29. package/corpus/templates/clips/changelog/2026-08-14-standardized-share-popovers.md +6 -0
  30. package/corpus/templates/clips/chrome-extension/public/manifest.json +11 -3
  31. package/corpus/templates/clips/chrome-extension/src/github-preview-content.ts +37 -8
  32. package/corpus/templates/clips/chrome-extension/src/github-preview.ts +3 -3
  33. package/corpus/templates/clips/chrome-extension/src/offscreen.ts +8 -25
  34. package/corpus/templates/clips/desktop/src/app.tsx +24 -0
  35. package/corpus/templates/clips/desktop/src/lib/media-capture-constraints.ts +42 -13
  36. package/corpus/templates/clips/desktop/src/lib/recorder.ts +11 -18
  37. package/corpus/templates/clips/desktop/src/overlays/onboarding.tsx +1 -0
  38. package/corpus/templates/clips/desktop/src/shared/config.ts +1 -0
  39. package/corpus/templates/clips/desktop/src/styles.css +24 -9
  40. package/corpus/templates/clips/desktop/src-tauri/Cargo.toml +3 -0
  41. package/corpus/templates/clips/desktop/src-tauri/src/config.rs +8 -0
  42. package/corpus/templates/clips/desktop/src-tauri/src/native_screen/custom_capture.rs +154 -176
  43. package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +115 -31
  44. package/corpus/templates/clips/desktop/src-tauri/src/rewind_clip.rs +7 -0
  45. package/corpus/templates/clips/server/db/index.ts +1 -0
  46. package/corpus/templates/clips/server/db/schema.ts +1 -0
  47. package/corpus/templates/clips/server/lib/calendar-event-meetings.ts +6 -1
  48. package/corpus/templates/clips/server/lib/emails.ts +23 -0
  49. package/corpus/templates/clips/server/lib/public-agent-context.ts +55 -0
  50. package/corpus/templates/clips/server/lib/recordings.ts +2 -1
  51. package/corpus/templates/clips/server/lib/resumable-upload-cleanup.ts +49 -0
  52. package/corpus/templates/clips/server/lib/s3-upload-provider.ts +34 -7
  53. package/corpus/templates/clips/server/lib/upload-interruption.ts +15 -1
  54. package/corpus/templates/clips/server/lib/upload-lease.ts +94 -9
  55. package/corpus/templates/clips/server/routes/api/agent-context.json.get.ts +82 -7
  56. package/corpus/templates/clips/server/routes/api/public-recording.get.ts +1 -0
  57. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/chunk.post.ts +71 -12
  58. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/interrupt.post.ts +4 -3
  59. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/reset-chunks.post.ts +19 -0
  60. package/corpus/templates/clips/server/routes/api/uploads/reap.post.ts +8 -1
  61. package/corpus/templates/clips/shared/recording-link.ts +4 -0
  62. package/corpus/templates/clips/shared/share-loader-response.ts +2 -1
  63. package/corpus/templates/content/app/components/editor/DocumentToolbar.tsx +7 -16
  64. package/corpus/templates/factory/AGENTS.md +5 -1
  65. package/corpus/templates/factory/README.md +6 -2
  66. package/corpus/templates/factory/actions/view-screen.ts +15 -0
  67. package/corpus/templates/factory/app/components/factory/FactoryAgentsView.tsx +58 -189
  68. package/corpus/templates/factory/app/components/layout/Header.tsx +1 -0
  69. package/corpus/templates/factory/app/components/layout/Layout.tsx +1 -1
  70. package/corpus/templates/factory/app/components/layout/Sidebar.tsx +7 -0
  71. package/corpus/templates/factory/app/hooks/use-navigation-state.ts +3 -0
  72. package/corpus/templates/factory/app/i18n/en-US.ts +3 -1
  73. package/corpus/templates/factory/app/routes/agents.tsx +9 -0
  74. package/corpus/templates/mail/server/plugins/auth.ts +0 -2
  75. package/corpus/templates/plan/app/components/plan/LocalCodebasePicker.tsx +50 -14
  76. package/corpus/templates/plan/app/pages/PlansPage.tsx +9 -19
  77. package/corpus/templates/slides/app/components/editor/AddSlidePopover.tsx +98 -29
  78. package/corpus/templates/slides/app/components/editor/EditorActionCluster.tsx +45 -91
  79. package/corpus/templates/slides/app/components/editor/EditorSidebar.tsx +94 -2
  80. package/corpus/templates/slides/app/components/editor/EditorToolbar.tsx +17 -29
  81. package/corpus/templates/slides/app/components/editor/HistoryPanel.tsx +7 -1
  82. package/corpus/templates/slides/app/components/editor/SlideContextToolbar.tsx +1 -1
  83. package/corpus/templates/slides/app/context/DeckContext.tsx +108 -19
  84. package/corpus/templates/slides/app/global.css +7 -40
  85. package/corpus/templates/slides/app/i18n/en-US.ts +4 -0
  86. package/corpus/templates/slides/app/lib/editor-shortcuts.ts +46 -0
  87. package/corpus/templates/slides/app/pages/DeckEditor.tsx +143 -45
  88. package/dist/agent/index.d.ts +1 -1
  89. package/dist/agent/production-agent.d.ts +30 -0
  90. package/dist/agent/production-agent.js +100 -8
  91. package/dist/agent/types.d.ts +9 -0
  92. package/dist/client/AgentPanel.d.ts +13 -1
  93. package/dist/client/AgentPanel.js +22 -17
  94. package/dist/client/AssistantChat.d.ts +15 -1
  95. package/dist/client/AssistantChat.js +135 -48
  96. package/dist/client/HighlightedCodeBlock.js +12 -2
  97. package/dist/client/MultiTabAssistantChat.js +44 -27
  98. package/dist/client/agent-chat/index.d.ts +1 -0
  99. package/dist/client/agent-chat/index.js +1 -0
  100. package/dist/client/chat/markdown-renderer.js +20 -5
  101. package/dist/client/chat/message-components.d.ts +2 -2
  102. package/dist/client/chat/message-components.js +69 -35
  103. package/dist/client/chat/run-recovery.d.ts +4 -1
  104. package/dist/client/chat/run-recovery.js +62 -30
  105. package/dist/client/chat/tool-call-display.d.ts +12 -3
  106. package/dist/client/chat/tool-call-display.js +79 -40
  107. package/dist/client/chat/widgets/DataChartRenderer.js +3 -1
  108. package/dist/client/chat/widgets/DataChartWidget.js +8 -2
  109. package/dist/client/chat/widgets/DataInsightsWidget.js +3 -1
  110. package/dist/client/chat/widgets/DataTableWidget.js +23 -9
  111. package/dist/client/chat/widgets/builtin-tool-renderers.js +3 -1
  112. package/dist/client/chat-first/apps-rail.js +1 -1
  113. package/dist/client/components/ui/message-scroller.d.ts +1 -1
  114. package/dist/client/components/ui/message-scroller.js +7 -3
  115. package/dist/client/composer/runtime-adapters.js +3 -2
  116. package/dist/client/context-xray/ContextMeter.js +5 -1
  117. package/dist/client/context-xray/ContextXRayPanel.js +5 -5
  118. package/dist/client/error-format.d.ts +4 -0
  119. package/dist/client/error-format.js +93 -5
  120. package/dist/client/i18n.js +83 -17
  121. package/dist/client/observability/ThumbsFeedback.js +10 -8
  122. package/dist/client/settings/AccountSettingsCard.js +48 -19
  123. package/dist/client/sharing/ShareButton.d.ts +1 -2
  124. package/dist/client/sharing/ShareButton.js +151 -81
  125. package/dist/client/sharing/ShareDialog.js +5 -4
  126. package/dist/client/use-chat-threads.js +10 -14
  127. package/dist/coding-tools/run-code.js +2 -1
  128. package/dist/coding-tools/sandbox/background.d.ts +1 -0
  129. package/dist/coding-tools/sandbox/background.js +8 -1
  130. package/dist/coding-tools/sandbox/executions-store.d.ts +2 -0
  131. package/dist/coding-tools/sandbox/executions-store.js +41 -3
  132. package/dist/collab/routes.d.ts +1 -1
  133. package/dist/collab/struct-routes.d.ts +1 -1
  134. package/dist/index.d.ts +1 -1
  135. package/dist/localization/core-messages/ar-SA.d.ts +3 -0
  136. package/dist/localization/core-messages/ar-SA.js +541 -0
  137. package/dist/localization/core-messages/de-DE.d.ts +3 -0
  138. package/dist/localization/core-messages/de-DE.js +513 -0
  139. package/dist/localization/core-messages/en-US.d.ts +513 -0
  140. package/dist/localization/core-messages/en-US.js +513 -0
  141. package/dist/localization/core-messages/es-ES.d.ts +3 -0
  142. package/dist/localization/core-messages/es-ES.js +520 -0
  143. package/dist/localization/core-messages/fr-FR.d.ts +3 -0
  144. package/dist/localization/core-messages/fr-FR.js +520 -0
  145. package/dist/localization/core-messages/hi-IN.d.ts +3 -0
  146. package/dist/localization/core-messages/hi-IN.js +513 -0
  147. package/dist/localization/core-messages/ja-JP.d.ts +3 -0
  148. package/dist/localization/core-messages/ja-JP.js +506 -0
  149. package/dist/localization/core-messages/ko-KR.d.ts +3 -0
  150. package/dist/localization/core-messages/ko-KR.js +506 -0
  151. package/dist/localization/core-messages/pt-BR.d.ts +3 -0
  152. package/dist/localization/core-messages/pt-BR.js +520 -0
  153. package/dist/localization/core-messages/zh-CN.d.ts +3 -0
  154. package/dist/localization/core-messages/zh-CN.js +506 -0
  155. package/dist/localization/core-messages/zh-TW.d.ts +3 -0
  156. package/dist/localization/core-messages/zh-TW.js +506 -0
  157. package/dist/localization/core-messages.d.ts +524 -0
  158. package/dist/localization/core-messages.js +124 -0
  159. package/dist/localization/default-messages.d.ts +3 -0
  160. package/dist/localization/default-messages.js +3 -0
  161. package/dist/notifications/routes.d.ts +1 -1
  162. package/dist/observability/routes.d.ts +5 -5
  163. package/dist/progress/routes.d.ts +1 -1
  164. package/dist/provider-api/actions/custom-provider-registration.d.ts +11 -11
  165. package/dist/provider-api/actions/provider-api.d.ts +15 -15
  166. package/dist/resources/handlers.d.ts +1 -1
  167. package/dist/secrets/routes.d.ts +9 -9
  168. package/dist/server/agent-chat/plugin-options.d.ts +7 -2
  169. package/dist/server/agent-chat-plugin.d.ts +10 -0
  170. package/dist/server/agent-chat-plugin.js +144 -41
  171. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  172. package/dist/server/agent-run-context.d.ts +7 -1
  173. package/dist/server/agent-run-context.js +9 -2
  174. package/dist/server/agent-teams-run-queue.d.ts +3 -0
  175. package/dist/server/agent-teams.js +38 -9
  176. package/dist/server/auth-marketing.d.ts +1 -0
  177. package/dist/server/auth-marketing.js +0 -2
  178. package/dist/server/auth.d.ts +1 -0
  179. package/dist/server/auth.js +18 -0
  180. package/dist/server/index.d.ts +1 -1
  181. package/dist/server/onboarding-html.d.ts +1 -0
  182. package/dist/server/onboarding-html.js +2 -88
  183. package/dist/server/realtime-token.d.ts +1 -1
  184. package/dist/server/request-context.d.ts +7 -0
  185. package/dist/server/request-context.js +15 -0
  186. package/dist/shared/markdown-block-split.d.ts +16 -0
  187. package/dist/shared/markdown-block-split.js +81 -5
  188. package/dist/styles/agent-native.css +13 -0
  189. package/dist/voice/voice-cleanup-prompt.js +1 -0
  190. package/docs/content/agent-surfaces.mdx +30 -0
  191. package/package.json +2 -2
  192. package/corpus/templates/clips/shared/mic-audio-cleanup.ts +0 -350
@@ -784,13 +784,40 @@ export const s3FileUploadProvider: FileUploadProvider = {
784
784
  )
785
785
  .join("") +
786
786
  "</CompleteMultipartUpload>";
787
- const res = await signedS3Request(cfg, meta.objectKey, {
788
- method: "POST",
789
- query: { uploadId: session.sessionId },
790
- body: new TextEncoder().encode(manifest),
791
- contentType: "application/xml",
792
- timeoutMs: S3_PUT_TIMEOUT_MS,
793
- });
787
+ let res: Response;
788
+ try {
789
+ res = await signedS3Request(cfg, meta.objectKey, {
790
+ method: "POST",
791
+ query: { uploadId: session.sessionId },
792
+ body: new TextEncoder().encode(manifest),
793
+ contentType: "application/xml",
794
+ timeoutMs: S3_PUT_TIMEOUT_MS,
795
+ });
796
+ } catch (error) {
797
+ // R2 can finish the object while the HTTP response is lost. Reconcile
798
+ // that ambiguous case before the caller aborts the provider session.
799
+ try {
800
+ if (await verifyCompletedMultipartObject(cfg, meta)) {
801
+ await deleteObject(cfg, meta.stagingKey).catch((cleanupError) => {
802
+ console.warn(
803
+ "[s3-upload] failed to delete multipart staging object:",
804
+ cleanupError instanceof Error
805
+ ? cleanupError.message
806
+ : String(cleanupError),
807
+ );
808
+ });
809
+ return publicObjectUrl(cfg, meta.objectKey);
810
+ }
811
+ } catch (verificationError) {
812
+ console.warn(
813
+ "[s3-upload] completion reconciliation failed:",
814
+ verificationError,
815
+ );
816
+ }
817
+ throw new Error(
818
+ `S3 CompleteMultipartUpload failed: ${error instanceof Error ? error.message : String(error)}`,
819
+ );
820
+ }
794
821
  const body = await res.text().catch(() => "");
795
822
  if (!res.ok || /<Error(?:\s|>)/.test(body)) {
796
823
  // CompleteMultipartUpload is not idempotent at the S3 API level. If
@@ -1,8 +1,22 @@
1
1
  export const RETRYABLE_UPLOAD_INTERRUPTION_REASON =
2
2
  "Upload was interrupted. The local recording is safe; retry from the Clips desktop app.";
3
3
 
4
+ export function retryableUploadInterruptionReason(
5
+ detail: string | null | undefined,
6
+ ): string {
7
+ const normalized = detail?.trim();
8
+ return normalized
9
+ ? `${RETRYABLE_UPLOAD_INTERRUPTION_REASON} Last error: ${normalized}`
10
+ : RETRYABLE_UPLOAD_INTERRUPTION_REASON;
11
+ }
12
+
4
13
  export function isRetryableUploadInterruption(
5
14
  failureReason: string | null | undefined,
6
15
  ): boolean {
7
- return failureReason === RETRYABLE_UPLOAD_INTERRUPTION_REASON;
16
+ return (
17
+ failureReason === RETRYABLE_UPLOAD_INTERRUPTION_REASON ||
18
+ failureReason?.startsWith(
19
+ `${RETRYABLE_UPLOAD_INTERRUPTION_REASON} Last error:`,
20
+ ) === true
21
+ );
8
22
  }
@@ -15,6 +15,8 @@ import { getDbExec, isPostgres } from "@agent-native/core/db";
15
15
  import { and, eq, inArray, isNull, sql } from "drizzle-orm";
16
16
 
17
17
  import { getDb, schema } from "../db/index.js";
18
+ import type { StoredResumableSession } from "./resumable-session.js";
19
+ import { abortResumableUploadSession } from "./resumable-upload-cleanup.js";
18
20
 
19
21
  /**
20
22
  * ponytail: one horizon for both in-progress statuses. A paused recorder emits
@@ -148,6 +150,61 @@ export interface ReapResult {
148
150
  expired: ReapedUpload[];
149
151
  failed: number;
150
152
  scratchKeysDeleted: number;
153
+ resumableSessionsAborted: number;
154
+ resumableCleanupFailed: number;
155
+ }
156
+
157
+ function resumableSessionKey(
158
+ recordingId: string,
159
+ generationId: string | null,
160
+ ): string {
161
+ return generationId
162
+ ? `resumable-session-${recordingId}-${generationId}`
163
+ : `resumable-session-${recordingId}`;
164
+ }
165
+
166
+ function parseStoredResumableSession(
167
+ value: unknown,
168
+ ): StoredResumableSession | null {
169
+ let parsed: unknown = value;
170
+ if (typeof parsed === "string") {
171
+ try {
172
+ parsed = JSON.parse(parsed);
173
+ } catch {
174
+ // coercion-ok: malformed persisted session is absent, not an active session.
175
+ return null;
176
+ }
177
+ }
178
+ if (!parsed || typeof parsed !== "object") return null;
179
+ const candidate = parsed as Record<string, unknown>;
180
+ if (
181
+ typeof candidate.providerId !== "string" ||
182
+ typeof candidate.sessionId !== "string" ||
183
+ !candidate.meta ||
184
+ typeof candidate.meta !== "object" ||
185
+ typeof candidate.bytesUploaded !== "number" ||
186
+ !Number.isFinite(candidate.bytesUploaded) ||
187
+ candidate.bytesUploaded < 0
188
+ ) {
189
+ return null;
190
+ }
191
+ return candidate as unknown as StoredResumableSession;
192
+ }
193
+
194
+ async function readResumableSessionState(
195
+ exec: ReturnType<typeof getDbExec>,
196
+ placeholder: string,
197
+ key: string,
198
+ ): Promise<{ present: boolean; session: StoredResumableSession | null }> {
199
+ const result = await exec.execute({
200
+ sql: `SELECT value FROM application_state WHERE key = ${placeholder}`,
201
+ args: [key],
202
+ });
203
+ const row = (result.rows as Array<{ value?: unknown }> | undefined)?.[0];
204
+ return {
205
+ present: row !== undefined,
206
+ session: row === undefined ? null : parseStoredResumableSession(row.value),
207
+ };
151
208
  }
152
209
 
153
210
  /**
@@ -196,6 +253,8 @@ export async function reapExpiredUploads(
196
253
  }));
197
254
 
198
255
  let failed = 0;
256
+ let resumableSessionsAborted = 0;
257
+ let resumableCleanupFailed = 0;
199
258
  if (expired.length > 0 && !dryRun) {
200
259
  const ids = expired.map((row) => row.id);
201
260
  const result = await exec.execute({
@@ -223,16 +282,35 @@ export async function reapExpiredUploads(
223
282
  failed = terminated.size;
224
283
 
225
284
  for (const id of terminated) {
226
- const generationId = expired.find(
227
- (row) => row.id === id,
228
- )?.uploadGenerationId;
285
+ const generationId =
286
+ expired.find((row) => row.id === id)?.uploadGenerationId ?? null;
287
+ const sessionKey = resumableSessionKey(id, generationId);
288
+ const sessionState = await readResumableSessionState(
289
+ exec,
290
+ p(1),
291
+ sessionKey,
292
+ );
293
+ if (sessionState.present && !sessionState.session) {
294
+ resumableCleanupFailed += 1;
295
+ console.warn(
296
+ `[upload-reaper-${id}] resumable session state is unreadable; keeping it for manual cleanup`,
297
+ );
298
+ continue;
299
+ }
300
+ if (sessionState.session) {
301
+ const cleaned = await abortResumableUploadSession(
302
+ sessionState.session,
303
+ { label: `upload-reaper-${id}` },
304
+ );
305
+ if (!cleaned) {
306
+ resumableCleanupFailed += 1;
307
+ continue;
308
+ }
309
+ resumableSessionsAborted += 1;
310
+ }
229
311
  await exec.execute({
230
312
  sql: `DELETE FROM application_state WHERE key = ${p(1)}`,
231
- args: [
232
- generationId
233
- ? `resumable-session-${id}-${generationId}`
234
- : `resumable-session-${id}`,
235
- ],
313
+ args: [sessionKey],
236
314
  });
237
315
  }
238
316
  }
@@ -241,7 +319,14 @@ export async function reapExpiredUploads(
241
319
  ? (await selectUnclaimedChunkKeys(limit)).length
242
320
  : await deleteUnclaimedChunkScratch(limit);
243
321
 
244
- return { dryRun, expired, failed, scratchKeysDeleted };
322
+ return {
323
+ dryRun,
324
+ expired,
325
+ failed,
326
+ scratchKeysDeleted,
327
+ resumableSessionsAborted,
328
+ resumableCleanupFailed,
329
+ };
245
330
  }
246
331
 
247
332
  /**
@@ -6,6 +6,7 @@
6
6
  * smaller agent-oriented shape plus discoverable transcript/frame APIs.
7
7
  */
8
8
 
9
+ import { asc, count, eq } from "drizzle-orm";
9
10
  import {
10
11
  defineEventHandler,
11
12
  getQuery,
@@ -13,6 +14,7 @@ import {
13
14
  type H3Event,
14
15
  } from "h3";
15
16
 
17
+ import { getDb, schema } from "../../db/index.js";
16
18
  import {
17
19
  applyAgentJsonHeaders,
18
20
  buildPublicAgentContext,
@@ -21,6 +23,7 @@ import {
21
23
  loadAgentCtas,
22
24
  loadAgentTranscript,
23
25
  loadPublicAgentAccess,
26
+ MAX_PUBLIC_AGENT_HISTORY_ITEMS,
24
27
  parseAgentChapters,
25
28
  queryString,
26
29
  CLIPS_AGENT_ACCESS_PARAM,
@@ -42,14 +45,76 @@ export default defineEventHandler(async (event: H3Event) => {
42
45
  }
43
46
 
44
47
  const recording = accessResult.access.recording;
45
- const [{ transcript, agentSegments }, ctas, browserDiagnostics, bugReport] =
46
- await Promise.all([
47
- loadAgentTranscript(recording.id, recording.durationMs),
48
- loadAgentCtas(recording.id),
49
- loadAgentBrowserDiagnostics(recording.id),
50
- loadAgentBugReport(recording.id),
51
- ]);
48
+ const db = getDb();
49
+ const historyQueryLimit = MAX_PUBLIC_AGENT_HISTORY_ITEMS + 1;
50
+ const [
51
+ { transcript, agentSegments },
52
+ commentRows,
53
+ reactionRows,
54
+ commentCountRows,
55
+ reactionCountRows,
56
+ ctas,
57
+ browserDiagnostics,
58
+ bugReport,
59
+ ] = await Promise.all([
60
+ loadAgentTranscript(recording.id, recording.durationMs),
61
+ recording.enableComments
62
+ ? db
63
+ .select({
64
+ id: schema.recordingComments.id,
65
+ recordingId: schema.recordingComments.recordingId,
66
+ threadId: schema.recordingComments.threadId,
67
+ parentId: schema.recordingComments.parentId,
68
+ authorName: schema.recordingComments.authorName,
69
+ content: schema.recordingComments.content,
70
+ videoTimestampMs: schema.recordingComments.videoTimestampMs,
71
+ resolved: schema.recordingComments.resolved,
72
+ createdAt: schema.recordingComments.createdAt,
73
+ updatedAt: schema.recordingComments.updatedAt,
74
+ })
75
+ .from(schema.recordingComments)
76
+ .where(eq(schema.recordingComments.recordingId, recording.id))
77
+ .orderBy(
78
+ asc(schema.recordingComments.videoTimestampMs),
79
+ asc(schema.recordingComments.createdAt),
80
+ )
81
+ .limit(historyQueryLimit)
82
+ : Promise.resolve([]),
83
+ recording.enableReactions
84
+ ? db
85
+ .select({
86
+ id: schema.recordingReactions.id,
87
+ emoji: schema.recordingReactions.emoji,
88
+ videoTimestampMs: schema.recordingReactions.videoTimestampMs,
89
+ viewerName: schema.recordingReactions.viewerName,
90
+ createdAt: schema.recordingReactions.createdAt,
91
+ })
92
+ .from(schema.recordingReactions)
93
+ .where(eq(schema.recordingReactions.recordingId, recording.id))
94
+ .orderBy(asc(schema.recordingReactions.createdAt))
95
+ .limit(historyQueryLimit)
96
+ : Promise.resolve([]),
97
+ recording.enableComments
98
+ ? db
99
+ .select({ count: count() })
100
+ .from(schema.recordingComments)
101
+ .where(eq(schema.recordingComments.recordingId, recording.id))
102
+ : Promise.resolve([{ count: 0 }]),
103
+ recording.enableReactions
104
+ ? db
105
+ .select({ count: count() })
106
+ .from(schema.recordingReactions)
107
+ .where(eq(schema.recordingReactions.recordingId, recording.id))
108
+ : Promise.resolve([{ count: 0 }]),
109
+ loadAgentCtas(recording.id),
110
+ loadAgentBrowserDiagnostics(recording.id),
111
+ loadAgentBugReport(recording.id),
112
+ ]);
52
113
  const chapters = parseAgentChapters(recording);
114
+ const comments = commentRows.slice(0, MAX_PUBLIC_AGENT_HISTORY_ITEMS);
115
+ const reactions = reactionRows.slice(0, MAX_PUBLIC_AGENT_HISTORY_ITEMS);
116
+ const commentCount = Number(commentCountRows[0]?.count ?? comments.length);
117
+ const reactionCount = Number(reactionCountRows[0]?.count ?? reactions.length);
53
118
 
54
119
  return buildPublicAgentContext({
55
120
  event,
@@ -57,6 +122,16 @@ export default defineEventHandler(async (event: H3Event) => {
57
122
  transcript,
58
123
  agentSegments,
59
124
  chapters,
125
+ comments,
126
+ reactions,
127
+ commentCount,
128
+ commentsTruncated:
129
+ commentRows.length > MAX_PUBLIC_AGENT_HISTORY_ITEMS ||
130
+ commentCount > comments.length,
131
+ reactionCount,
132
+ reactionsTruncated:
133
+ reactionRows.length > MAX_PUBLIC_AGENT_HISTORY_ITEMS ||
134
+ reactionCount > reactions.length,
60
135
  ctas,
61
136
  browserDiagnostics,
62
137
  bugReport,
@@ -263,6 +263,7 @@ export default defineEventHandler(async (event) => {
263
263
  !viewerIsOrgMember &&
264
264
  !tokenAllowsAgentAccess
265
265
  ) {
266
+ setResponseHeader(event, "Cache-Control", "private, max-age=0, no-store");
266
267
  setResponseStatus(event, 404);
267
268
  return { error: "Not found" };
268
269
  }
@@ -52,6 +52,7 @@ import {
52
52
  setResumableSession,
53
53
  type StoredResumableSession,
54
54
  } from "../../../../lib/resumable-session.js";
55
+ import { abortResumableUploadSession } from "../../../../lib/resumable-upload-cleanup.js";
55
56
  import { resolveResumableUploadProvider } from "../../../../lib/resumable-upload-provider.js";
56
57
  import { isStreamingUploadDisabled } from "../../../../lib/streaming-upload-mode.js";
57
58
  import { renewUploadLease } from "../../../../lib/upload-lease.js";
@@ -854,6 +855,23 @@ async function handleResumableChunk(
854
855
  `[resumable-chunk-${recordingId}] resumable session exists - bytesUploaded=${session.bytesUploaded} index=${index} isFinal=${isFinal}`,
855
856
  );
856
857
 
858
+ const cleanupFailedFinalSession = async () => {
859
+ const cleaned = await abortResumableUploadSession(session, {
860
+ provider: uploadProvider,
861
+ label: `resumable-final-${recordingId}`,
862
+ });
863
+ if (cleaned) {
864
+ await deleteResumableSession(recordingId, uploadGenerationId).catch(
865
+ (error) =>
866
+ console.warn(
867
+ `[resumable-chunk-${recordingId}] failed to retire aborted session:`,
868
+ error,
869
+ ),
870
+ );
871
+ }
872
+ return !cleaned;
873
+ };
874
+
857
875
  const raw = await readRawBody(event, false);
858
876
  const bytes: Uint8Array = raw ?? new Uint8Array(0);
859
877
  let finalizedSourceSizeBytes = session.bytesUploaded;
@@ -884,19 +902,37 @@ async function handleResumableChunk(
884
902
  // 0-byte sentinel from the recorder after stop(). All data chunks have
885
903
  // already been PUT to the provider; send Content-Range: bytes */<total>
886
904
  // to close the session before handing off to finalize-recording.
887
- const closeRes = await uploadProvider.resumable.relayChunk(
888
- { sessionId: session.sessionId, meta: session.meta },
889
- `bytes */${session.bytesUploaded}`,
890
- new Uint8Array(0),
891
- );
905
+ let closeRes;
906
+ try {
907
+ closeRes = await uploadProvider.resumable.relayChunk(
908
+ { sessionId: session.sessionId, meta: session.meta },
909
+ `bytes */${session.bytesUploaded}`,
910
+ new Uint8Array(0),
911
+ );
912
+ } catch (error) {
913
+ const cleanupFailed = await cleanupFailedFinalSession();
914
+ const detail = error instanceof Error ? error.message : String(error);
915
+ console.error(
916
+ `[resumable-chunk-${recordingId}] session close threw:`,
917
+ error,
918
+ );
919
+ setResponseStatus(event, 502);
920
+ return {
921
+ ok: false,
922
+ error: `Resumable session close failed: ${detail}`,
923
+ ...(cleanupFailed ? { cleanupFailed: true } : {}),
924
+ };
925
+ }
892
926
  if (!closeRes.ok || closeRes.status === 308) {
893
927
  console.error(
894
928
  `[resumable-chunk-${recordingId}] session close failed (${closeRes.status})`,
895
929
  );
930
+ const cleanupFailed = await cleanupFailedFinalSession();
896
931
  setResponseStatus(event, 502);
897
932
  return {
898
933
  ok: false,
899
934
  error: `Resumable session close failed (${closeRes.status})`,
935
+ ...(cleanupFailed ? { cleanupFailed: true } : {}),
900
936
  };
901
937
  }
902
938
  if (closeRes.updatedMeta) {
@@ -964,12 +1000,31 @@ async function handleResumableChunk(
964
1000
  : `bytes ${start}-${end}/*`;
965
1001
 
966
1002
  const putT0 = Date.now();
967
- const putResult = await uploadProvider.resumable.relayChunk(
968
- { sessionId: session.sessionId, meta: session.meta },
969
- contentRange,
970
- bytes,
971
- { mimeType: mimeType.split(";")[0].trim() },
972
- );
1003
+ let putResult;
1004
+ try {
1005
+ putResult = await uploadProvider.resumable.relayChunk(
1006
+ { sessionId: session.sessionId, meta: session.meta },
1007
+ contentRange,
1008
+ bytes,
1009
+ { mimeType: mimeType.split(";")[0].trim() },
1010
+ );
1011
+ } catch (error) {
1012
+ if (isFinal) {
1013
+ const cleanupFailed = await cleanupFailedFinalSession();
1014
+ const detail = error instanceof Error ? error.message : String(error);
1015
+ console.error(
1016
+ `[resumable-chunk-${recordingId}] final chunk upload threw:`,
1017
+ error,
1018
+ );
1019
+ setResponseStatus(event, 502);
1020
+ return {
1021
+ ok: false,
1022
+ error: `Final chunk upload failed: ${detail}`,
1023
+ ...(cleanupFailed ? { cleanupFailed: true } : {}),
1024
+ };
1025
+ }
1026
+ throw error;
1027
+ }
973
1028
  console.log(
974
1029
  `[resumable-chunk-${recordingId}] PUT ${Date.now() - putT0}ms status=${putResult.status} range="${contentRange}"`,
975
1030
  );
@@ -980,7 +1035,10 @@ async function handleResumableChunk(
980
1035
  if (!resultOk) {
981
1036
  const restartRequired =
982
1037
  putResult.status === 404 || putResult.status === 410;
983
- if (restartRequired) {
1038
+ const cleanupFailed = isFinal
1039
+ ? await cleanupFailedFinalSession()
1040
+ : false;
1041
+ if (restartRequired && !cleanupFailed) {
984
1042
  await deleteResumableSession(recordingId, uploadGenerationId).catch(
985
1043
  () => {},
986
1044
  );
@@ -990,6 +1048,7 @@ async function handleResumableChunk(
990
1048
  ok: false,
991
1049
  error: `Chunk upload failed (${putResult.status})`,
992
1050
  ...(restartRequired ? { restartRequired: true } : {}),
1051
+ ...(cleanupFailed ? { cleanupFailed: true } : {}),
993
1052
  };
994
1053
  }
995
1054
 
@@ -31,7 +31,7 @@ import {
31
31
  } from "../../../../lib/recordings.js";
32
32
  import {
33
33
  isRetryableUploadInterruption,
34
- RETRYABLE_UPLOAD_INTERRUPTION_REASON,
34
+ retryableUploadInterruptionReason,
35
35
  } from "../../../../lib/upload-interruption.js";
36
36
  import abortUpload from "./abort.post.js";
37
37
 
@@ -131,12 +131,13 @@ export default defineEventHandler(async (event: H3Event) => {
131
131
  const uploadStateRaw = await readAppState(uploadStateKey);
132
132
  const uploadState = uploadStateRaw ?? {};
133
133
  const interruptedAt = new Date().toISOString();
134
+ const failureReason = retryableUploadInterruptionReason(interruptionDetail);
134
135
  if (!alreadyInterrupted) {
135
136
  const interrupted = await db
136
137
  .update(schema.recordings)
137
138
  .set({
138
139
  status: "failed",
139
- failureReason: RETRYABLE_UPLOAD_INTERRUPTION_REASON,
140
+ failureReason,
140
141
  updatedAt: interruptedAt,
141
142
  })
142
143
  .where(
@@ -167,7 +168,7 @@ export default defineEventHandler(async (event: H3Event) => {
167
168
  ...uploadState,
168
169
  recordingId,
169
170
  status: "failed",
170
- failureReason: RETRYABLE_UPLOAD_INTERRUPTION_REASON,
171
+ failureReason,
171
172
  retryableInterruption: true,
172
173
  uploadAttemptId: attemptId,
173
174
  uploadGenerationId,
@@ -55,8 +55,10 @@ import {
55
55
  } from "../../../../lib/recordings.js";
56
56
  import {
57
57
  deleteResumableSession,
58
+ getResumableSession,
58
59
  setResumableSession,
59
60
  } from "../../../../lib/resumable-session.js";
61
+ import { abortResumableUploadSession } from "../../../../lib/resumable-upload-cleanup.js";
60
62
  import { shouldEnableStreamingUpload } from "../../../../lib/streaming-upload-mode.js";
61
63
  import {
62
64
  renewUploadLease,
@@ -217,6 +219,10 @@ export default defineEventHandler(async (event: H3Event) => {
217
219
  const nextGenerationId = useGenerationFence ? randomUUID() : null;
218
220
  const uploadStateKey = `recording-upload-${recordingId}`;
219
221
  const uploadStateSnapshot = await readAppState(uploadStateKey);
222
+ const discardedResumableSession = await getResumableSession(
223
+ recordingId,
224
+ existingGenerationId,
225
+ );
220
226
  const reset = await db
221
227
  .update(schema.recordings)
222
228
  .set({
@@ -251,6 +257,19 @@ export default defineEventHandler(async (event: H3Event) => {
251
257
  };
252
258
  }
253
259
 
260
+ if (discardedResumableSession) {
261
+ const cleaned = await abortResumableUploadSession(
262
+ discardedResumableSession,
263
+ { label: `reset-${recordingId}` },
264
+ );
265
+ if (!cleaned) {
266
+ setResponseStatus(event, 502);
267
+ return {
268
+ error:
269
+ "The previous recording upload could not be cleaned up. Retry the upload restart.",
270
+ };
271
+ }
272
+ }
254
273
  const cleared = await deleteRecordingChunks(
255
274
  ownerEmail,
256
275
  recordingId,
@@ -59,10 +59,17 @@ export default defineEventHandler(async (event: H3Event) => {
59
59
  const result = await reapExpiredUploads({
60
60
  dryRun: query.dryRun === "1" || query.dryRun === "true",
61
61
  });
62
- if (result.failed > 0 || result.scratchKeysDeleted > 0) {
62
+ if (
63
+ result.failed > 0 ||
64
+ result.scratchKeysDeleted > 0 ||
65
+ result.resumableSessionsAborted > 0 ||
66
+ result.resumableCleanupFailed > 0
67
+ ) {
63
68
  console.log("[uploads] reaped expired uploads", {
64
69
  failed: result.failed,
65
70
  scratchKeysDeleted: result.scratchKeysDeleted,
71
+ resumableSessionsAborted: result.resumableSessionsAborted,
72
+ resumableCleanupFailed: result.resumableCleanupFailed,
66
73
  });
67
74
  }
68
75
  return { ok: true, ...result };
@@ -12,6 +12,10 @@
12
12
  */
13
13
  import { withShareAttribution } from "./share-attribution";
14
14
 
15
+ /** Short-lived capability used by a signed-in private share viewer to request access. */
16
+ export const CLIPS_ACCESS_REQUEST_TOKEN_PREFIX = "clips-access-request";
17
+ export const CLIPS_ACCESS_REQUEST_TOKEN_TTL_SECONDS = 10 * 60;
18
+
15
19
  /** Public share path for a recording, relative to the app base path. */
16
20
  export function recordingSharePath(recordingId: string): string {
17
21
  return `/share/${encodeURIComponent(recordingId)}`;
@@ -5,8 +5,9 @@ export const PRIVATE_SHARE_RESPONSE_HEADERS = {
5
5
  "Referrer-Policy": "no-referrer",
6
6
  };
7
7
 
8
- export function privateShareLoaderData<T>(payload: T) {
8
+ export function privateShareLoaderData<T>(payload: T, status = 200) {
9
9
  return data(payload, {
10
+ status,
10
11
  headers: PRIVATE_SHARE_RESPONSE_HEADERS,
11
12
  });
12
13
  }
@@ -6,6 +6,7 @@ import { useT } from "@agent-native/core/client/i18n";
6
6
  import { ShareButton } from "@agent-native/core/client/sharing";
7
7
  import { CreativeContextShareTab } from "@agent-native/creative-context/client";
8
8
  import { PresenceBar } from "@agent-native/toolkit/collab-ui";
9
+ import { ShareTrigger } from "@agent-native/toolkit/sharing";
9
10
  import type { DocumentSourceInfo } from "@shared/api";
10
11
  import {
11
12
  IconArrowBarDown,
@@ -31,7 +32,6 @@ import {
31
32
  IconLink,
32
33
  IconMessageCircle,
33
34
  IconRefresh,
34
- IconShare3,
35
35
  IconTrash,
36
36
  } from "@tabler/icons-react";
37
37
  import { useQueryClient } from "@tanstack/react-query";
@@ -891,22 +891,13 @@ export function DocumentToolbar({
891
891
  className="mr-1"
892
892
  />
893
893
  {isLocalFileDocument ? (
894
- <Button
895
- size="sm"
896
- variant="outline"
897
- className="h-9 gap-1.5 rounded-lg px-3"
894
+ <ShareTrigger
895
+ className="h-9 rounded-lg px-3"
896
+ pending={shareLocalFile.isPending}
898
897
  disabled={shareLocalFile.isPending}
899
- onClick={() => void handleShareLocalFile()}
900
- >
901
- {shareLocalFile.isPending ? (
902
- <IconLoader2 className="h-4 w-4 animate-spin" />
903
- ) : (
904
- <IconShare3 className="h-4 w-4" />
905
- )}
906
- <span className="hidden sm:inline">
907
- {t("editor.toolbar.share")}
908
- </span>
909
- </Button>
898
+ label={t("editor.toolbar.share")}
899
+ onPress={() => void handleShareLocalFile()}
900
+ />
910
901
  ) : (
911
902
  <>
912
903
  <ShareButton
@@ -35,7 +35,8 @@ decisions, feedback, agent runs, and provider audit records.
35
35
 
36
36
  ## Application state
37
37
 
38
- - `navigation.view`: `factory` when the workspace is open.
38
+ - `navigation.view`: `factory` for the Factory workspace or `agents` for the
39
+ shared Agents sidebar surface.
39
40
  - `navigation.factoryId`: selected Factory id when present.
40
41
  - `navigation.factoryTab`: `map` | `inbox` | `rules` | `automations` | `agents` | `audit` | `settings`.
41
42
  - `navigation.factoryAuditRunId`: selected automation run in the audit view when present.
@@ -43,6 +44,9 @@ decisions, feedback, agent runs, and provider audit records.
43
44
  - A selected graph node or edge is part of `navigation` context. Read
44
45
  `view-screen` before answering why a route exists or changing the selected
45
46
  Factory.
47
+ - The `/agents` surface shows mounted agentic apps and reusable agent packs from
48
+ the same Dispatch SQL/action registry. Use `view-screen` there before
49
+ answering questions about the visible agent inventory.
46
50
 
47
51
  ## Action contract
48
52