@agent-native/core 0.101.2 → 0.101.4

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 (90) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +25 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/client/RunStuckBanner.tsx +12 -5
  5. package/corpus/core/src/client/sse-event-processor.ts +36 -1
  6. package/corpus/core/src/client/use-run-stuck-detection.ts +162 -25
  7. package/corpus/core/src/credentials/index.ts +41 -8
  8. package/corpus/core/src/deploy/build.ts +78 -25
  9. package/corpus/core/src/extensions/fetch-tool.ts +33 -3
  10. package/corpus/core/src/notifications/channels.ts +20 -6
  11. package/corpus/core/src/secrets/crypto.ts +26 -10
  12. package/corpus/core/src/secrets/storage.ts +16 -3
  13. package/corpus/core/src/server/agent-chat-plugin.ts +60 -7
  14. package/corpus/core/src/server/derived-secret.ts +5 -1
  15. package/corpus/templates/analytics/changelog/2026-07-14-long-running-cross-app-analyses-now-continue-reliably-in-the.md +6 -0
  16. package/corpus/templates/analytics/server/plugins/agent-chat.ts +8 -4
  17. package/corpus/templates/chat/changelog/2026-07-14-chat-opens-reliably-on-hosted-deployments-instead-of-failing.md +6 -0
  18. package/corpus/templates/clips/app/components/player/media-duration.ts +24 -0
  19. package/corpus/templates/clips/app/components/player/share-dialog.tsx +44 -81
  20. package/corpus/templates/clips/app/components/player/video-player.tsx +9 -18
  21. package/corpus/templates/clips/app/i18n/ar-SA.ts +0 -8
  22. package/corpus/templates/clips/app/i18n/de-DE.ts +0 -9
  23. package/corpus/templates/clips/app/i18n/en-US.ts +0 -8
  24. package/corpus/templates/clips/app/i18n/es-ES.ts +0 -9
  25. package/corpus/templates/clips/app/i18n/fr-FR.ts +0 -9
  26. package/corpus/templates/clips/app/i18n/hi-IN.ts +0 -8
  27. package/corpus/templates/clips/app/i18n/ja-JP.ts +0 -9
  28. package/corpus/templates/clips/app/i18n/ko-KR.ts +0 -9
  29. package/corpus/templates/clips/app/i18n/pt-BR.ts +0 -9
  30. package/corpus/templates/clips/app/i18n/zh-CN.ts +0 -7
  31. package/corpus/templates/clips/app/i18n/zh-TW.ts +0 -7
  32. package/corpus/templates/clips/changelog/2026-07-14-paused-time-no-longer-counts-toward-chrome-extension-recordi.md +6 -0
  33. package/corpus/templates/clips/changelog/2026-07-14-share-dialogs-show-separate-links-for-people-and-agents.md +6 -0
  34. package/corpus/templates/clips/chrome-extension/src/offscreen.ts +30 -4
  35. package/corpus/templates/clips/chrome-extension/src/recording-duration.ts +32 -0
  36. package/corpus/templates/dispatch/changelog/2026-07-14-dispatch-now-distinguishes-connected-agents-from-locally-mou.md +6 -0
  37. package/corpus/templates/slides/actions/_uploaded-files.ts +13 -2
  38. package/corpus/templates/slides/actions/import-docx.ts +3 -9
  39. package/corpus/templates/slides/actions/import-file.ts +11 -14
  40. package/corpus/templates/slides/actions/import-pptx.ts +3 -9
  41. package/corpus/templates/slides/changelog/2026-07-14-powerpoint-template-uploads-now-work-in-hosted-slides-deploy.md +6 -0
  42. package/corpus/templates/slides/server/handlers/uploads.ts +100 -50
  43. package/corpus/templates/slides/server/lib/tenant-files.ts +38 -11
  44. package/corpus/templates/slides/server/lib/uploaded-reference-storage.ts +108 -0
  45. package/dist/client/RunStuckBanner.d.ts.map +1 -1
  46. package/dist/client/RunStuckBanner.js +9 -5
  47. package/dist/client/RunStuckBanner.js.map +1 -1
  48. package/dist/client/sse-event-processor.d.ts.map +1 -1
  49. package/dist/client/sse-event-processor.js +33 -1
  50. package/dist/client/sse-event-processor.js.map +1 -1
  51. package/dist/client/use-run-stuck-detection.d.ts +4 -4
  52. package/dist/client/use-run-stuck-detection.d.ts.map +1 -1
  53. package/dist/client/use-run-stuck-detection.js +104 -19
  54. package/dist/client/use-run-stuck-detection.js.map +1 -1
  55. package/dist/collab/awareness.d.ts +2 -2
  56. package/dist/collab/awareness.d.ts.map +1 -1
  57. package/dist/credentials/index.d.ts +12 -7
  58. package/dist/credentials/index.d.ts.map +1 -1
  59. package/dist/credentials/index.js +33 -8
  60. package/dist/credentials/index.js.map +1 -1
  61. package/dist/deploy/build.d.ts +15 -14
  62. package/dist/deploy/build.d.ts.map +1 -1
  63. package/dist/deploy/build.js +64 -23
  64. package/dist/deploy/build.js.map +1 -1
  65. package/dist/extensions/fetch-tool.d.ts +13 -1
  66. package/dist/extensions/fetch-tool.d.ts.map +1 -1
  67. package/dist/extensions/fetch-tool.js +9 -2
  68. package/dist/extensions/fetch-tool.js.map +1 -1
  69. package/dist/notifications/channels.js +20 -4
  70. package/dist/notifications/channels.js.map +1 -1
  71. package/dist/observability/routes.d.ts +1 -1
  72. package/dist/progress/routes.d.ts +1 -1
  73. package/dist/resources/handlers.d.ts +1 -1
  74. package/dist/secrets/crypto.d.ts +15 -7
  75. package/dist/secrets/crypto.d.ts.map +1 -1
  76. package/dist/secrets/crypto.js +23 -10
  77. package/dist/secrets/crypto.js.map +1 -1
  78. package/dist/secrets/storage.d.ts.map +1 -1
  79. package/dist/secrets/storage.js +16 -3
  80. package/dist/secrets/storage.js.map +1 -1
  81. package/dist/server/agent-chat-plugin.d.ts +20 -0
  82. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  83. package/dist/server/agent-chat-plugin.js +38 -4
  84. package/dist/server/agent-chat-plugin.js.map +1 -1
  85. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  86. package/dist/server/derived-secret.d.ts +1 -1
  87. package/dist/server/derived-secret.d.ts.map +1 -1
  88. package/dist/server/derived-secret.js.map +1 -1
  89. package/package.json +2 -2
  90. package/corpus/templates/clips/app/components/sharing/slack-share-hint.tsx +0 -68
@@ -19,6 +19,7 @@ import {
19
19
  useState,
20
20
  } from "react";
21
21
 
22
+ import { resolveMediaDurationMs } from "@/components/player/media-duration";
22
23
  import {
23
24
  DropdownMenu,
24
25
  DropdownMenuContent,
@@ -824,22 +825,16 @@ export const VideoPlayer = forwardRef<VideoPlayerHandle, VideoPlayerProps>(
824
825
  durationProbedRef.current = false;
825
826
  }, [activeVideoSrc, durationMs]);
826
827
 
827
- // The recorder's elapsed-time counter (durationMs prop) is the most
828
- // trustworthy length we have. A MediaRecorder WebM's own duration is
829
- // cluster-estimated and lands short by up to one timeslice, so we never
830
- // let it overwrite a real prop — doing so makes the scrubber jump to a
831
- // shorter length than the actual recording on first watch.
832
- const hasReliableDurationProp =
833
- Number.isFinite(durationMs) && durationMs > 0;
834
-
828
+ // Prefer the recorder's elapsed-time counter for ordinary WebM timeslice
829
+ // drift, but let clearly different playable-media metadata win. This also
830
+ // repairs playback controls for older clips whose stored duration counted
831
+ // time spent paused.
835
832
  const probeDurationIfNeeded = useCallback(
836
833
  (v: HTMLVideoElement) => {
837
834
  if (durationProbedRef.current) return;
838
835
  if (Number.isFinite(v.duration) && v.duration > 0) {
839
836
  durationProbedRef.current = true;
840
- if (!hasReliableDurationProp) {
841
- setResolvedDurationMs(Math.round(v.duration * 1000));
842
- }
837
+ setResolvedDurationMs(resolveMediaDurationMs(durationMs, v.duration));
843
838
  return;
844
839
  }
845
840
  if (playAttemptPendingRef.current || !v.paused) return;
@@ -855,7 +850,7 @@ export const VideoPlayer = forwardRef<VideoPlayerHandle, VideoPlayerProps>(
855
850
  // picks up the real duration.
856
851
  }
857
852
  },
858
- [hasReliableDurationProp],
853
+ [durationMs],
859
854
  );
860
855
 
861
856
  // Resolve the WebM-duration-is-Infinity Chrome quirk: when a video created
@@ -873,11 +868,7 @@ export const VideoPlayer = forwardRef<VideoPlayerHandle, VideoPlayerProps>(
873
868
 
874
869
  const onDurationChange = () => {
875
870
  if (Number.isFinite(v.duration) && v.duration > 0) {
876
- // Don't downgrade a trustworthy recorder duration to the
877
- // cluster-estimated WebM duration; only adopt it as a fallback.
878
- if (!hasReliableDurationProp) {
879
- setResolvedDurationMs(Math.round(v.duration * 1000));
880
- }
871
+ setResolvedDurationMs(resolveMediaDurationMs(durationMs, v.duration));
881
872
  // After we've resolved the real duration, rewind back to 0 so the
882
873
  // user isn't sitting at the end of the clip.
883
874
  if (durationProbedRef.current && v.currentTime > v.duration) {
@@ -900,7 +891,7 @@ export const VideoPlayer = forwardRef<VideoPlayerHandle, VideoPlayerProps>(
900
891
  v.removeEventListener("loadedmetadata", onLoadedMetadata);
901
892
  v.removeEventListener("durationchange", onDurationChange);
902
893
  };
903
- }, [activeVideoSrc, hasReliableDurationProp, probeDurationIfNeeded]);
894
+ }, [activeVideoSrc, durationMs, probeDurationIfNeeded]);
904
895
 
905
896
  // Reset the thumbnail-capture flag when the source changes (e.g. the
906
897
  // player is reused for a different recording via React Router).
@@ -887,14 +887,6 @@ const messages = {
887
887
  "سيفقد {{email}} الوصول إلى هذه المؤسسة. يمكنك دائمًا دعوته مرة أخرى.",
888
888
  remove: "إزالة",
889
889
  },
890
- slackShareHint: {
891
- playsInline: "يعمل داخل Slack",
892
- connectedDescription:
893
- "الصق هذا الرابط في أي مساحة عمل متصلة لتشغيله داخلها.",
894
- makeInline: "اجعله يعمل داخل Slack",
895
- connectDescription: "قم بتوصيل مساحة عمل حتى يظهر هذا الرابط كفيديو.",
896
- connect: "اتصال",
897
- },
898
890
  commentsPanel: {
899
891
  disabled: "تم تعطيل التعليقات لهذا التسجيل.",
900
892
  beFirst: "كن أول من يعلق",
@@ -911,15 +911,6 @@ Alle sichtbaren Änderungen für Clips-Nutzer werden hier dokumentiert. Du kanns
911
911
  "{{email}} verliert den Zugriff auf diese Organisation. Du kannst die Person jederzeit erneut einladen.",
912
912
  remove: "Entfernen",
913
913
  },
914
- slackShareHint: {
915
- playsInline: "Wird inline in Slack abgespielt",
916
- connectedDescription:
917
- "Füge diesen Link in einem verbundenen Workspace ein, um ihn inline abzuspielen.",
918
- makeInline: "Inline-Wiedergabe in Slack aktivieren",
919
- connectDescription:
920
- "Verbinde einen Workspace, damit dieser Link als Video angezeigt wird.",
921
- connect: "Verbinden",
922
- },
923
914
  commentsPanel: {
924
915
  disabled: "Kommentare sind für diese Aufzeichnung deaktiviert.",
925
916
  beFirst: "Schreibe den ersten Kommentar",
@@ -882,14 +882,6 @@ All notable user-facing changes to Clips are documented here. Open it any time f
882
882
  "{{email}} will lose access to this organization. You can always invite them back.",
883
883
  remove: "Remove",
884
884
  },
885
- slackShareHint: {
886
- playsInline: "Plays inline in Slack",
887
- connectedDescription:
888
- "Paste this link in any connected workspace to play it inline.",
889
- makeInline: "Make it play inline in Slack",
890
- connectDescription: "Connect a workspace so this link unfurls as a video.",
891
- connect: "Connect",
892
- },
893
885
  commentsPanel: {
894
886
  disabled: "Comments are disabled for this recording.",
895
887
  beFirst: "Be the first to comment",
@@ -904,15 +904,6 @@ Todos los cambios visibles para los usuarios de Clips se documentan aquí. Puede
904
904
  "{{email}} perderá acceso a esta organización. Siempre puedes volver a invitarlo.",
905
905
  remove: "Eliminar",
906
906
  },
907
- slackShareHint: {
908
- playsInline: "Se reproduce inline en Slack",
909
- connectedDescription:
910
- "Pega este enlace en cualquier espacio conectado para reproducirlo inline.",
911
- makeInline: "Haz que se reproduzca inline en Slack",
912
- connectDescription:
913
- "Conecta un espacio para que este enlace se despliegue como video.",
914
- connect: "Conectar",
915
- },
916
907
  commentsPanel: {
917
908
  disabled: "Los comentarios están desactivados para esta grabación.",
918
909
  beFirst: "Sé la primera persona en comentar",
@@ -906,15 +906,6 @@ Tous les changements visibles par les utilisateurs de Clips sont documentés ici
906
906
  "{{email}} perdra l’accès à cette organisation. Vous pourrez toujours l’inviter à nouveau.",
907
907
  remove: "Retirer",
908
908
  },
909
- slackShareHint: {
910
- playsInline: "Lecture inline dans Slack",
911
- connectedDescription:
912
- "Collez ce lien dans n’importe quel espace connecté pour le lire inline.",
913
- makeInline: "Le lire inline dans Slack",
914
- connectDescription:
915
- "Connectez un espace pour que ce lien se déplie en vidéo.",
916
- connect: "Connecter",
917
- },
918
909
  commentsPanel: {
919
910
  disabled: "Les commentaires sont désactivés pour cet enregistrement.",
920
911
  beFirst: "Soyez le premier à commenter",
@@ -876,14 +876,6 @@ Clips में उपयोगकर्ताओं को दिखने व
876
876
  "{{email}} इस संगठन तक पहुंच खो देगा। आप उन्हें हमेशा वापस आमंत्रित कर सकते हैं।",
877
877
  remove: "हटाएं",
878
878
  },
879
- slackShareHint: {
880
- playsInline: "Slack में इनलाइन चलता है",
881
- connectedDescription:
882
- "इसे इनलाइन चलाने के लिए यह लिंक किसी भी जुड़े हुए वर्कस्पेस में पेस्ट करें।",
883
- makeInline: "इसे Slack में इनलाइन चलाएं",
884
- connectDescription: "एक वर्कस्पेस कनेक्ट करें ताकि यह लिंक वीडियो के रूप में खुले।",
885
- connect: "कनेक्ट करें",
886
- },
887
879
  commentsPanel: {
888
880
  disabled: "इस रिकॉर्डिंग के लिए टिप्पणियां बंद हैं।",
889
881
  beFirst: "पहली टिप्पणी करें",
@@ -893,15 +893,6 @@ Clips のユーザー向けの主な変更はここに記録されます。コ
893
893
  "{{email}} はこの組織へのアクセスを失います。いつでも再招待できます。",
894
894
  remove: "削除",
895
895
  },
896
- slackShareHint: {
897
- playsInline: "Slack 内でインライン再生",
898
- connectedDescription:
899
- "このリンクを接続済みワークスペースに貼り付けるとインライン再生されます。",
900
- makeInline: "Slack でインライン再生する",
901
- connectDescription:
902
- "ワークスペースを接続すると、このリンクが動画として展開されます。",
903
- connect: "接続",
904
- },
905
896
  commentsPanel: {
906
897
  disabled: "この録画ではコメントが無効です。",
907
898
  beFirst: "最初にコメントする",
@@ -883,15 +883,6 @@ Clips의 모든 사용자 대상 변경 사항은 여기에 기록됩니다. 명
883
883
  "{{email}}은 이 조직에 대한 액세스 권한을 잃습니다. 언제든 다시 초대할 수 있습니다.",
884
884
  remove: "제거",
885
885
  },
886
- slackShareHint: {
887
- playsInline: "Slack에서 인라인 재생",
888
- connectedDescription:
889
- "이 링크를 연결된 워크스페이스에 붙여넣으면 인라인으로 재생됩니다.",
890
- makeInline: "Slack에서 인라인 재생하기",
891
- connectDescription:
892
- "워크스페이스를 연결하면 이 링크가 동영상으로 펼쳐집니다.",
893
- connect: "연결",
894
- },
895
886
  commentsPanel: {
896
887
  disabled: "이 녹화에서는 댓글이 비활성화되어 있습니다.",
897
888
  beFirst: "첫 댓글을 남겨보세요",
@@ -900,15 +900,6 @@ Todas as mudanças visíveis para usuários do Clips são documentadas aqui. Voc
900
900
  "{{email}} perderá acesso a esta organização. Você sempre pode convidá-lo novamente.",
901
901
  remove: "Remover",
902
902
  },
903
- slackShareHint: {
904
- playsInline: "Reproduz inline no Slack",
905
- connectedDescription:
906
- "Cole este link em qualquer workspace conectado para reproduzi-lo inline.",
907
- makeInline: "Faça reproduzir inline no Slack",
908
- connectDescription:
909
- "Conecte um workspace para que este link abra como vídeo.",
910
- connect: "Conectar",
911
- },
912
903
  commentsPanel: {
913
904
  disabled: "Os comentários estão desativados para esta gravação.",
914
905
  beFirst: "Seja a primeira pessoa a comentar",
@@ -849,13 +849,6 @@ Clips 中所有面向用户的重要更改都会记录在这里。你可以随
849
849
  "{{email}} 将失去对此组织的访问权限。你随时可以重新邀请他们。",
850
850
  remove: "移除",
851
851
  },
852
- slackShareHint: {
853
- playsInline: "可在 Slack 中内联播放",
854
- connectedDescription: "将此链接粘贴到任何已连接工作区即可内联播放。",
855
- makeInline: "让它在 Slack 中内联播放",
856
- connectDescription: "连接一个工作区,让此链接展开为视频。",
857
- connect: "连接",
858
- },
859
852
  commentsPanel: {
860
853
  disabled: "此录制已禁用评论。",
861
854
  beFirst: "成为第一个评论的人",
@@ -842,13 +842,6 @@ const messages = {
842
842
  "{{email}} 將失去對此組織的存取權限。你隨時可以重新邀請他們。",
843
843
  remove: "移除",
844
844
  },
845
- slackShareHint: {
846
- playsInline: "可在 Slack 中行內播放",
847
- connectedDescription: "將此連結貼上到任何已連線工作區即可行內播放。",
848
- makeInline: "讓它在 Slack 中行內播放",
849
- connectDescription: "連線一個工作區,讓此連結展開為影片。",
850
- connect: "連線",
851
- },
852
845
  commentsPanel: {
853
846
  disabled: "此錄製已停用評論。",
854
847
  beFirst: "成為第一個評論的人",
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-07-14
4
+ ---
5
+
6
+ Paused time no longer counts toward Chrome extension recording durations.
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: improved
3
+ date: 2026-07-14
4
+ ---
5
+
6
+ The Share dialog once again makes separate links for people and agents easy to find.
@@ -36,6 +36,13 @@ import {
36
36
  import { MAX_UPLOAD_BYTES } from "@shared/upload-limits";
37
37
 
38
38
  import { waitForReadyRecordingAfterFinalizeError } from "./finalize-recovery";
39
+ import {
40
+ createRecordingDurationState,
41
+ pauseRecordingDuration,
42
+ resumeRecordingDuration,
43
+ startRecordingDuration,
44
+ type RecordingDurationState,
45
+ } from "./recording-duration";
39
46
  import { captureExtensionError, initExtensionSentry } from "./sentry";
40
47
 
41
48
  initExtensionSentry("offscreen");
@@ -187,6 +194,7 @@ type ActiveRecording = {
187
194
  authToken: string | null;
188
195
  mode: CaptureMode;
189
196
  startedAtMs: number;
197
+ duration: RecordingDurationState;
190
198
  mimeType: string;
191
199
  recorder: MediaRecorder;
192
200
  outputStream: MediaStream;
@@ -1218,6 +1226,7 @@ async function begin(message: BeginMessage): Promise<{
1218
1226
  authToken: message.authToken ?? null,
1219
1227
  mode: ready.mode,
1220
1228
  startedAtMs: 0,
1229
+ duration: createRecordingDurationState(),
1221
1230
  mimeType,
1222
1231
  recorder,
1223
1232
  outputStream,
@@ -1350,6 +1359,7 @@ function startRecorderNow(recording: ActiveRecording): void {
1350
1359
  playStartChime();
1351
1360
  recording.recorder.start(2000);
1352
1361
  recording.startedAtMs = Date.now();
1362
+ recording.duration = startRecordingDuration(recording.startedAtMs);
1353
1363
  console.log("[clips-offscreen] recorder.start ok");
1354
1364
  reportStatus(recording.sessionId, "recording", {
1355
1365
  recordingId: recording.recordingId,
@@ -1446,6 +1456,11 @@ async function finalizeStop(recording: ActiveRecording): Promise<void> {
1446
1456
  recording.resolveStopped({ ok: true, status: "cancelled" });
1447
1457
  return;
1448
1458
  }
1459
+ // Freeze the media duration as soon as MediaRecorder stops. Waiting for
1460
+ // outstanding uploads below must not make the clip appear longer, and time
1461
+ // spent paused is excluded by pauseRecordingDuration().
1462
+ recording.duration = pauseRecordingDuration(recording.duration, Date.now());
1463
+ const durationMs = recording.duration.elapsedMs;
1449
1464
  reportStatus(recording.sessionId, "uploading", {
1450
1465
  recordingId: recording.recordingId,
1451
1466
  });
@@ -1460,7 +1475,6 @@ async function finalizeStop(recording: ActiveRecording): Promise<void> {
1460
1475
  ? rejected.reason
1461
1476
  : new Error(String(rejected.reason));
1462
1477
  }
1463
- const durationMs = Math.max(0, Date.now() - recording.startedAtMs);
1464
1478
  // Surface WHY a finalize might fail before the server's cryptic "No chunks
1465
1479
  // found": 0 chunks means the recorder emitted no non-empty data (empty
1466
1480
  // capture / never started), which is a different problem than an auth 401.
@@ -1549,7 +1563,7 @@ async function finalizeStop(recording: ActiveRecording): Promise<void> {
1549
1563
  recordingId: recording.recordingId,
1550
1564
  chunkCount: recording.chunkIndex,
1551
1565
  mimeType: recording.mimeType,
1552
- durationMs: Math.max(0, Date.now() - recording.startedAtMs),
1566
+ durationMs,
1553
1567
  },
1554
1568
  });
1555
1569
  // The upload failed — save the buffered recording to disk so it isn't lost.
@@ -1574,7 +1588,13 @@ async function finalizeStop(recording: ActiveRecording): Promise<void> {
1574
1588
  function pause(message: SimpleMessage): { ok: boolean } {
1575
1589
  const recording = activeRecording;
1576
1590
  if (recording && recording.sessionId === message.sessionId) {
1577
- if (recording.recorder.state === "recording") recording.recorder.pause();
1591
+ if (recording.recorder.state === "recording") {
1592
+ recording.recorder.pause();
1593
+ recording.duration = pauseRecordingDuration(
1594
+ recording.duration,
1595
+ Date.now(),
1596
+ );
1597
+ }
1578
1598
  reportStatus(recording.sessionId, "paused", {
1579
1599
  recordingId: recording.recordingId,
1580
1600
  });
@@ -1585,7 +1605,13 @@ function pause(message: SimpleMessage): { ok: boolean } {
1585
1605
  function resume(message: SimpleMessage): { ok: boolean } {
1586
1606
  const recording = activeRecording;
1587
1607
  if (recording && recording.sessionId === message.sessionId) {
1588
- if (recording.recorder.state === "paused") recording.recorder.resume();
1608
+ if (recording.recorder.state === "paused") {
1609
+ recording.recorder.resume();
1610
+ recording.duration = resumeRecordingDuration(
1611
+ recording.duration,
1612
+ Date.now(),
1613
+ );
1614
+ }
1589
1615
  reportStatus(recording.sessionId, "recording", {
1590
1616
  recordingId: recording.recordingId,
1591
1617
  });
@@ -0,0 +1,32 @@
1
+ export type RecordingDurationState = {
2
+ elapsedMs: number;
3
+ activeSinceMs: number | null;
4
+ };
5
+
6
+ export function createRecordingDurationState(): RecordingDurationState {
7
+ return { elapsedMs: 0, activeSinceMs: null };
8
+ }
9
+
10
+ export function startRecordingDuration(nowMs: number): RecordingDurationState {
11
+ return { elapsedMs: 0, activeSinceMs: nowMs };
12
+ }
13
+
14
+ export function pauseRecordingDuration(
15
+ state: RecordingDurationState,
16
+ nowMs: number,
17
+ ): RecordingDurationState {
18
+ if (state.activeSinceMs === null) return state;
19
+ return {
20
+ elapsedMs:
21
+ state.elapsedMs + Math.max(0, Math.round(nowMs - state.activeSinceMs)),
22
+ activeSinceMs: null,
23
+ };
24
+ }
25
+
26
+ export function resumeRecordingDuration(
27
+ state: RecordingDurationState,
28
+ nowMs: number,
29
+ ): RecordingDurationState {
30
+ if (state.activeSinceMs !== null) return state;
31
+ return { ...state, activeSinceMs: nowMs };
32
+ }
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-07-14
4
+ ---
5
+
6
+ Dispatch now distinguishes connected agents from locally mounted apps when routing cross-app work.
@@ -4,11 +4,19 @@ import path from "path";
4
4
  import { getRequestUserEmail } from "@agent-native/core/server/request-context";
5
5
 
6
6
  import { tenantUploadDir } from "../server/lib/tenant-files.js";
7
+ import { readUploadedReferenceBlob } from "../server/lib/uploaded-reference-storage.js";
7
8
 
8
- export function resolveUserUploadedFile(filePath: string): string {
9
+ export async function readUserUploadedFile(
10
+ filePath: string,
11
+ ): Promise<{ data: Buffer; filename: string }> {
9
12
  const email = getRequestUserEmail();
10
13
  if (!email) throw new Error("no authenticated user");
11
14
 
15
+ const privateUpload = await readUploadedReferenceBlob(filePath, email);
16
+ if (privateUpload) {
17
+ return privateUpload;
18
+ }
19
+
12
20
  const allowedDir = tenantUploadDir(email);
13
21
  const absPath = path.isAbsolute(filePath)
14
22
  ? filePath
@@ -23,5 +31,8 @@ export function resolveUserUploadedFile(filePath: string): string {
23
31
  if (!fs.existsSync(resolved)) {
24
32
  throw new Error(`File not found: ${filePath}`);
25
33
  }
26
- return resolved;
34
+ return {
35
+ data: await fs.promises.readFile(resolved),
36
+ filename: path.basename(resolved),
37
+ };
27
38
  }
@@ -1,11 +1,9 @@
1
- import fs from "fs";
2
-
3
1
  import { defineAction } from "@agent-native/core";
4
2
  import { z } from "zod";
5
3
 
6
4
  import { parseDocx } from "../server/handlers/import/docx-parser.js";
7
5
  import { convertSectionsToSlides } from "../server/handlers/import/html-converter.js";
8
- import { resolveUserUploadedFile } from "./_uploaded-files.js";
6
+ import { readUserUploadedFile } from "./_uploaded-files.js";
9
7
 
10
8
  export default defineAction({
11
9
  description:
@@ -16,9 +14,7 @@ export default defineAction({
16
14
  schema: z.object({
17
15
  filePath: z
18
16
  .string()
19
- .describe(
20
- "Server path to the uploaded DOCX file (e.g. data/uploads/document.docx)",
21
- ),
17
+ .describe("Uploaded DOCX path or opaque hosted upload reference"),
22
18
  convertToSlides: z
23
19
  .boolean()
24
20
  .optional()
@@ -28,9 +24,7 @@ export default defineAction({
28
24
  ),
29
25
  }),
30
26
  run: async ({ filePath, convertToSlides }) => {
31
- const absPath = resolveUserUploadedFile(filePath);
32
-
33
- const fileBuffer = await fs.promises.readFile(absPath);
27
+ const { data: fileBuffer } = await readUserUploadedFile(filePath);
34
28
  const doc = await parseDocx(fileBuffer);
35
29
 
36
30
  const result: Record<string, unknown> = {
@@ -1,4 +1,3 @@
1
- import fs from "fs";
2
1
  import path from "path";
3
2
 
4
3
  import { defineAction } from "@agent-native/core";
@@ -15,7 +14,7 @@ import { z } from "zod";
15
14
  import { getDb, schema } from "../server/db/index.js";
16
15
  import { notifyClients } from "../server/handlers/decks.js";
17
16
  import { upsertBuilderProxyDesignSystem } from "../server/lib/builder-design-system-proxy.js";
18
- import { resolveUserUploadedFile } from "./_uploaded-files.js";
17
+ import { readUserUploadedFile } from "./_uploaded-files.js";
19
18
 
20
19
  const DEFAULT_MAX_SOURCE_CHARS = 60_000;
21
20
 
@@ -30,9 +29,7 @@ export default defineAction({
30
29
  schema: z.object({
31
30
  filePath: z
32
31
  .string()
33
- .describe(
34
- "Server path to the uploaded file (e.g. data/uploads/file.pptx)",
35
- ),
32
+ .describe("Uploaded file path or opaque hosted upload reference"),
36
33
  format: z
37
34
  .enum(["pptx", "docx", "pdf", "fig", "auto"])
38
35
  .optional()
@@ -60,15 +57,15 @@ export default defineAction({
60
57
  ),
61
58
  }),
62
59
  run: async ({ filePath, format, deckId, importIntoDeck, maxChars }) => {
63
- const absPath = resolveUserUploadedFile(filePath);
60
+ const uploaded = await readUserUploadedFile(filePath);
64
61
  const sourceLimit = maxChars ?? DEFAULT_MAX_SOURCE_CHARS;
65
-
66
- const fileBuffer = await fs.promises.readFile(absPath);
62
+ const fileBuffer = uploaded.data;
63
+ const filename = uploaded.filename;
67
64
 
68
65
  // Detect format from extension if auto
69
66
  let detectedFormat = format;
70
67
  if (detectedFormat === "auto") {
71
- const ext = path.extname(absPath).toLowerCase();
68
+ const ext = path.extname(filename).toLowerCase();
72
69
  if (ext === ".pptx") detectedFormat = "pptx";
73
70
  else if (ext === ".docx") detectedFormat = "docx";
74
71
  else if (ext === ".pdf") detectedFormat = "pdf";
@@ -86,12 +83,12 @@ export default defineAction({
86
83
  "Figma .fig imports start Builder design-system indexing, not slide replacements. Re-run without importIntoDeck.",
87
84
  );
88
85
  }
89
- const title = titleFromPath(absPath);
86
+ const title = titleFromPath(filename);
90
87
  const result = await startBuilderDesignSystemIndex({
91
88
  projectName: title,
92
89
  files: [
93
90
  {
94
- name: path.basename(absPath),
91
+ name: path.basename(filename),
95
92
  data: fileBuffer,
96
93
  mimeType: "application/octet-stream",
97
94
  },
@@ -126,7 +123,7 @@ export default defineAction({
126
123
  const { convertToSlideHtml } =
127
124
  await import("../server/handlers/import/html-converter.js");
128
125
  const presentation = await parsePptx(fileBuffer);
129
- const title = presentation.title || titleFromPath(absPath);
126
+ const title = presentation.title || titleFromPath(filename);
130
127
 
131
128
  if (importIntoDeck) {
132
129
  if (!deckId) throw new Error("deckId is required to import into deck");
@@ -172,7 +169,7 @@ export default defineAction({
172
169
  await import("../server/handlers/import/html-converter.js");
173
170
  const doc = await parseDocx(fileBuffer);
174
171
  const slideHtmlArray = convertSectionsToSlides(doc.sections);
175
- const title = doc.title || titleFromPath(absPath);
172
+ const title = doc.title || titleFromPath(filename);
176
173
 
177
174
  if (importIntoDeck) {
178
175
  if (!deckId) throw new Error("deckId is required to import into deck");
@@ -221,7 +218,7 @@ export default defineAction({
221
218
  const result = await pdf.getText();
222
219
  const pages = normalizePdfPages(result);
223
220
  const textPages = pages.filter((p) => p.text.trim());
224
- const title = titleFromPath(absPath);
221
+ const title = titleFromPath(filename);
225
222
 
226
223
  if (textPages.length === 0) {
227
224
  throw new Error(
@@ -1,5 +1,3 @@
1
- import fs from "fs";
2
-
3
1
  import { defineAction } from "@agent-native/core";
4
2
  import { writeAppState } from "@agent-native/core/application-state";
5
3
  import {
@@ -15,7 +13,7 @@ import { notifyClients } from "../server/handlers/decks.js";
15
13
  import { convertToSlideHtml } from "../server/handlers/import/html-converter.js";
16
14
  import { parsePptx } from "../server/handlers/import/pptx-parser.js";
17
15
  import { getDeckUrl } from "./_app-url.js";
18
- import { resolveUserUploadedFile } from "./_uploaded-files.js";
16
+ import { readUserUploadedFile } from "./_uploaded-files.js";
19
17
 
20
18
  export default defineAction({
21
19
  description:
@@ -26,9 +24,7 @@ export default defineAction({
26
24
  schema: z.object({
27
25
  filePath: z
28
26
  .string()
29
- .describe(
30
- "Server path to the uploaded PPTX file (e.g. data/uploads/presentation.pptx)",
31
- ),
27
+ .describe("Uploaded PPTX path or opaque hosted upload reference"),
32
28
  deckId: z
33
29
  .string()
34
30
  .optional()
@@ -43,9 +39,7 @@ export default defineAction({
43
39
  ),
44
40
  }),
45
41
  run: async ({ filePath, deckId, title }) => {
46
- const absPath = resolveUserUploadedFile(filePath);
47
-
48
- const fileBuffer = await fs.promises.readFile(absPath);
42
+ const { data: fileBuffer } = await readUserUploadedFile(filePath);
49
43
  const presentation = await parsePptx(fileBuffer);
50
44
 
51
45
  const deckTitle = title || presentation.title || "Imported Presentation";
@@ -0,0 +1,6 @@
1
+ ---
2
+ type: fixed
3
+ date: 2026-07-14
4
+ ---
5
+
6
+ PowerPoint template uploads now work in hosted Slides deployments.