diff --git a/frontend/libs/components/promptbox/src/lib/PromptBoxAudio.spec.tsx b/frontend/libs/components/promptbox/src/lib/PromptBoxAudio.spec.tsx
new file mode 100644
--- /dev/null
+++ b/frontend/libs/components/promptbox/src/lib/PromptBoxAudio.spec.tsx
@@ -0,0 +1,144 @@
+import { act } from "react";
+import { createRoot, type Root } from "react-dom/client";
+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
+import { PromptBoxAudio } from "./PromptBoxAudio";
+import { usePromptAudioStore, type RefAudio } from "./promptStore";
+import type { OmniGenAudioModelDetails, UploadMediaFn } from "@storyteller/api";
+import { UploaderStates } from "@storyteller/common";
+import * as gallery from "@storyteller/ui-gallery-modal";
+
+const TRACK: RefAudio = {
+  id: "old", url: "https://example.com/old.mp3", file: new File([], "old.mp3"),
+  mediaToken: "old-token", duration: 5,
+};
+const MODELS = [
+  { model: "many", full_name: "Many", audio_references_supported: true, audio_references_max: 3, image_references_supported: true },
+  { model: "one", full_name: "One", audio_references_supported: true, audio_references_max: 1 },
+  { model: "none", full_name: "None", audio_references_supported: false, audio_references_max: 0 },
+] as OmniGenAudioModelDetails[];
+
+let root: Root;
+let container: HTMLDivElement;
+let probes: HTMLAudioElement[];
+let imageUpload: Parameters<UploadMediaFn>[0];
+let finishImageUpload: () => void;
+
+describe("audio prompt library completion", () => {
+  beforeEach(async () => {
+    (globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
+    usePromptAudioStore.setState({ referenceAudios: [TRACK], referenceImages: [], selectedModelId: "many", prompt: "" });
+    probes = [];
+    vi.stubGlobal("URL", class extends URL {
+      static createObjectURL = vi.fn(() => "blob:test");
+      static revokeObjectURL = vi.fn();
+    });
+    const createElement = document.createElement.bind(document);
+    vi.spyOn(document, "createElement").mockImplementation((tag: string, options?: ElementCreationOptions) => {
+      const element = createElement(tag, options);
+      if (tag === "audio") probes.push(element as HTMLAudioElement);
+      return element;
+    });
+    container = document.createElement("div");
+    document.body.append(container);
+    root = createRoot(container);
+    const uploadImage: UploadMediaFn = (args) => new Promise((resolve) => {
+      imageUpload = args;
+      finishImageUpload = resolve;
+    });
+    await act(async () => root.render(<PromptBoxAudio models={MODELS} uploadImage={uploadImage} />));
+  });
+
+  afterEach(async () => {
+    await act(async () => root.unmount());
+    container.remove();
+    vi.restoreAllMocks();
+    vi.unstubAllGlobals();
+  });
+
+  it("does not restore a removed reference after pending library metadata arrives", async () => {
+    await select([{ id: "new", fullImage: "https://example.com/new.mp3" }]);
+    await clickLabel("Remove audio");
+    await finishMetadata(10);
+    expect(usePromptAudioStore.getState().referenceAudios.map((a) => a.mediaToken)).toEqual(["new"]);
+  });
+
+  it("preserves a reference added while library metadata was pending", async () => {
+    await select([{ id: "new", fullImage: "https://example.com/new.mp3" }]);
+    await act(async () => usePromptAudioStore.getState().setReferenceAudios([TRACK, { ...TRACK, id: "other", mediaToken: "other-token" }]));
+    await finishMetadata(10);
+    expect(usePromptAudioStore.getState().referenceAudios.map((a) => a.mediaToken)).toEqual(["old-token", "other-token", "new"]);
+  });
+
+  it("uses the current model's count limit after metadata completes", async () => {
+    await select([{ id: "new", fullImage: "https://example.com/new.mp3" }]);
+    await act(async () => usePromptAudioStore.getState().setSelectedModelId("one"));
+    await finishMetadata(10);
+    expect(usePromptAudioStore.getState().referenceAudios).toEqual([TRACK]);
+  });
+
+  it("does not append audio when the current model no longer supports it", async () => {
+    await select([{ id: "new", fullImage: "https://example.com/new.mp3" }]);
+    await act(async () => usePromptAudioStore.getState().setSelectedModelId("none"));
+    await finishMetadata(10);
+    expect(usePromptAudioStore.getState().referenceAudios).toEqual([TRACK]);
+  });
+
+  it("checks total duration against references added during metadata loading", async () => {
+    await select([{ id: "new", fullImage: "https://example.com/new.mp3" }]);
+    await act(async () => usePromptAudioStore.getState().setReferenceAudios([{ ...TRACK, duration: 595 }]));
+    await finishMetadata(10);
+    expect(usePromptAudioStore.getState().referenceAudios).toHaveLength(1);
+    expect(usePromptAudioStore.getState().referenceAudios[0].duration).toBe(595);
+  });
+
+  it("clears a current image when completing a valid audio selection", async () => {
+    await select([{ id: "new", fullImage: "https://example.com/new.mp3" }]);
+    await act(async () => usePromptAudioStore.getState().setReferenceImages([{ id: "image", url: "image.png", mediaToken: "image-token", file: new File([], "image.png") }]));
+    await finishMetadata(10);
+    expect(usePromptAudioStore.getState().referenceImages).toEqual([]);
+  });
+
+  it("keeps ordinary library selection with known duration working", async () => {
+    await select([{ id: "new", fullImage: "https://example.com/new.mp3", durationMillis: 10000 }]);
+    await act(async () => { await (gallery as unknown as { pendingSelection: Promise<void> }).pendingSelection; });
+    expect(probes).toHaveLength(0);
+    expect(usePromptAudioStore.getState().referenceAudios.map((a) => a.mediaToken)).toEqual(["old-token", "new"]);
+  });
+
+  it("clears audio added while an image upload was pending", async () => {
+    await act(async () => usePromptAudioStore.getState().setReferenceAudios([]));
+    const input = container.querySelector<HTMLInputElement>('input[accept="image/*"]')!;
+    Object.defineProperty(input, "files", { value: [new File([], "image.png", { type: "image/png" })] });
+    await act(async () => input.dispatchEvent(new Event("change", { bubbles: true })));
+    await act(async () => usePromptAudioStore.getState().setReferenceAudios([TRACK]));
+    await act(async () => {
+      imageUpload.progressCallback({ status: UploaderStates.success, data: "uploaded-image" });
+      finishImageUpload();
+    });
+    expect(usePromptAudioStore.getState().referenceAudios).toEqual([]);
+    expect(usePromptAudioStore.getState().referenceImages.map((image) => image.mediaToken)).toEqual(["uploaded-image"]);
+  });
+});
+
+async function select(items: unknown[]) {
+  (gallery as unknown as { choose: (items: unknown[]) => void }).choose(items);
+  const open = Array.from(container.querySelectorAll("button")).find((button) => button.textContent === "From library");
+  if (!open) throw new Error("Missing live reference-row library button");
+  await act(async () => open.click());
+  await clickLabel("Use library selection");
+}
+
+async function clickLabel(label: string) {
+  const button = container.querySelector<HTMLButtonElement>(`[aria-label="${label}"]`);
+  if (!button) throw new Error(`Missing ${label}`);
+  await act(async () => button.click());
+}
+
+async function finishMetadata(duration: number) {
+  expect(probes).toHaveLength(1);
+  await act(async () => {
+    Object.defineProperty(probes[0], "duration", { value: duration });
+    probes[0].dispatchEvent(new Event("loadedmetadata"));
+    await (gallery as unknown as { pendingSelection: Promise<void> }).pendingSelection;
+  });
+}
diff --git a/frontend/libs/components/promptbox/src/lib/PromptBoxAudio.tsx b/frontend/libs/components/promptbox/src/lib/PromptBoxAudio.tsx
--- a/frontend/libs/components/promptbox/src/lib/PromptBoxAudio.tsx
+++ b/frontend/libs/components/promptbox/src/lib/PromptBoxAudio.tsx
@@ -1,4 +1,4 @@
-import { useMemo, useRef, useState, ReactNode } from "react";
+import { useLayoutEffect, useMemo, useRef, useState, ReactNode } from "react";
 import { toast } from "@storyteller/ui-toaster";
 import {
   GalleryModal,
@@ -167,6 +167,10 @@ export const PromptBoxAudio = ({
   const imageRefsSupported = supports(
     selectedModel?.image_references_supported,
   );
+  const audioLibraryLimitRef = useRef(audioRefsSupported ? maxAudioRefs : 0);
+  useLayoutEffect(() => {
+    audioLibraryLimitRef.current = audioRefsSupported ? maxAudioRefs : 0;
+  }, [audioRefsSupported, maxAudioRefs]);
   const requiresAudioRef = AUDIO_MODELS_REQUIRING_AUDIO_REF.has(
     selectedModel?.model ?? "",
   );
@@ -202,7 +206,7 @@ export const PromptBoxAudio = ({
   // Seed Audio can't combine audio and image references — adding one kind
   // clears the other so the request is always valid.
   const handleReferenceAudiosChange = (audios: typeof referenceAudios) => {
-    if (audios.length > 0 && referenceImages.length > 0) {
+    if (audios.length > 0 && usePromptAudioStore.getState().referenceImages.length > 0) {
       setReferenceImages([]);
       toast.error("Removed image reference — it can't be combined with audio");
     }
@@ -261,8 +265,11 @@ export const PromptBoxAudio = ({
       );
 
       const added: RefAudio[] = [];
-      let total = referenceAudios.reduce((sum, a) => sum + a.duration, 0);
+      const currentAudios = usePromptAudioStore.getState().referenceAudios;
+      const currentSlots = Math.max(0, audioLibraryLimitRef.current - currentAudios.length);
+      let total = currentAudios.reduce((sum, a) => sum + a.duration, 0);
       for (let i = 0; i < picked.length; i++) {
+        if (added.length >= currentSlots) break;
         const item = picked[i]!;
         const duration = durations[i]!;
         if (total + duration > AUDIO_REF_MAX_DURATION_SECONDS) {
@@ -281,7 +288,7 @@ export const PromptBoxAudio = ({
         });
       }
       if (added.length > 0) {
-        handleReferenceAudiosChange([...referenceAudios, ...added]);
+        handleReferenceAudiosChange([...currentAudios, ...added]);
       }
     } finally {
       setIsAudioLibraryProcessing(false);
@@ -290,7 +297,7 @@ export const PromptBoxAudio = ({
   };
 
   const handleReferenceImagesChange = (images: typeof referenceImages) => {
-    if (images.length > 0 && referenceAudios.length > 0) {
+    if (images.length > 0 && usePromptAudioStore.getState().referenceAudios.length > 0) {
       setReferenceAudios([]);
       toast.error("Removed audio reference — it can't be combined with an image");
     }
diff --git a/frontend/libs/components/promptbox/src/lib/common/AudioReferenceRow.tsx b/frontend/libs/components/promptbox/src/lib/common/AudioReferenceRow.tsx
--- a/frontend/libs/components/promptbox/src/lib/common/AudioReferenceRow.tsx
+++ b/frontend/libs/components/promptbox/src/lib/common/AudioReferenceRow.tsx
@@ -2,6 +2,7 @@ import {
   forwardRef,
   useCallback,
   useImperativeHandle,
+  useLayoutEffect,
   useRef,
   useState,
 } from "react";
@@ -73,9 +74,15 @@ export const AudioReferenceRow = forwardRef<
 
   // Latest-value refs so async upload callbacks append to fresh state.
   const referenceAudiosRef = useRef(referenceAudios);
-  referenceAudiosRef.current = referenceAudios;
   const referenceImagesRef = useRef(referenceImages);
-  referenceImagesRef.current = referenceImages;
+  const limitsRef = useRef({ maxAudioCount, maxAudioRefDuration, imageSupported });
+  useLayoutEffect(() => {
+    referenceAudiosRef.current = referenceAudios;
+    referenceImagesRef.current = referenceImages;
+    limitsRef.current = { maxAudioCount, maxAudioRefDuration, imageSupported };
+  }, [
+    referenceAudios, referenceImages, maxAudioCount, maxAudioRefDuration, imageSupported,
+  ]);
 
   const processAudioFiles = async (files: File[]) => {
     if (files.length === 0) return;
@@ -90,13 +97,14 @@ export const AudioReferenceRow = forwardRef<
 
     for (const file of filesToProcess) {
       const duration = await getAudioFileDuration(file);
+      if (referenceAudiosRef.current.length >= limitsRef.current.maxAudioCount) break;
       const currentTotal = referenceAudiosRef.current.reduce(
         (sum, audio) => sum + audio.duration,
         0,
       );
-      if (currentTotal + duration > maxAudioRefDuration) {
+      if (currentTotal + duration > limitsRef.current.maxAudioRefDuration) {
         toast.error(
-          `Total audio duration cannot exceed ${maxAudioRefDuration}s`,
+          `Total audio duration cannot exceed ${limitsRef.current.maxAudioRefDuration}s`,
         );
         break;
       }
@@ -108,6 +116,14 @@ export const AudioReferenceRow = forwardRef<
           assetFile: file,
           progressCallback: (newState) => {
             if (newState.status === UploaderStates.success && newState.data) {
+              setIsUploadingAudio(false);
+              const current = referenceAudiosRef.current;
+              if (current.length >= limitsRef.current.maxAudioCount) return;
+              const currentDuration = current.reduce((sum, audio) => sum + audio.duration, 0);
+              if (currentDuration + duration > limitsRef.current.maxAudioRefDuration) {
+                toast.error(`Total audio duration cannot exceed ${limitsRef.current.maxAudioRefDuration}s`);
+                return;
+              }
               const refAudio: RefAudio = {
                 id: Math.random().toString(36).substring(7),
                 url: URL.createObjectURL(file),
@@ -115,11 +131,8 @@ export const AudioReferenceRow = forwardRef<
                 mediaToken: newState.data,
                 duration,
               };
-              setIsUploadingAudio(false);
-              onReferenceAudiosChange([
-                ...referenceAudiosRef.current,
-                refAudio,
-              ]);
+              referenceAudiosRef.current = [...current, refAudio];
+              onReferenceAudiosChange(referenceAudiosRef.current);
             } else if (
               newState.status === UploaderStates.assetError ||
               newState.status === UploaderStates.imageCreateError
@@ -149,13 +162,15 @@ export const AudioReferenceRow = forwardRef<
       assetFile: file,
       progressCallback: (newState) => {
         if (newState.status === UploaderStates.success && newState.data) {
+          setIsUploadingImage(false);
+          if (!limitsRef.current.imageSupported) return;
           const refImage: RefImage = {
             id: Math.random().toString(36).substring(7),
             url: URL.createObjectURL(file),
             file,
             mediaToken: newState.data,
           };
-          setIsUploadingImage(false);
+          referenceImagesRef.current = [refImage];
           onReferenceImagesChange?.([refImage]);
         } else if (
           newState.status === UploaderStates.assetError ||
@@ -187,7 +202,8 @@ export const AudioReferenceRow = forwardRef<
   );
 
   const removeAudio = (id: string) => {
-    onReferenceAudiosChange(referenceAudios.filter((a) => a.id !== id));
+    referenceAudiosRef.current = referenceAudiosRef.current.filter((a) => a.id !== id);
+    onReferenceAudiosChange(referenceAudiosRef.current);
     if (audioInputRef.current) audioInputRef.current.value = "";
   };
 
diff --git a/frontend/libs/components/promptbox/src/lib/common/AudioReferenceRow.upload.spec.tsx b/frontend/libs/components/promptbox/src/lib/common/AudioReferenceRow.upload.spec.tsx
new file mode 100644
--- /dev/null
+++ b/frontend/libs/components/promptbox/src/lib/common/AudioReferenceRow.upload.spec.tsx
@@ -0,0 +1,165 @@
+import { act, createRef, useState, type RefObject } from "react";
+import { createRoot, type Root } from "react-dom/client";
+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
+import { UploaderStates } from "@storyteller/common";
+import { AudioReferenceRow, type AudioReferenceRowHandle } from "./AudioReferenceRow";
+import type { RefAudio } from "../promptStore";
+import type { UploadMediaFn } from "@storyteller/api";
+
+const FILE = new File(["audio"], "track.mp3", { type: "audio/mpeg" });
+const EXISTING: RefAudio = { id: "existing", url: "existing.mp3", file: FILE, mediaToken: "existing-token", duration: 595 };
+
+let root: Root;
+let container: HTMLDivElement;
+let handle: RefObject<AudioReferenceRowHandle | null>;
+let probes: HTMLAudioElement[];
+let uploads: Array<{ args: Parameters<UploadMediaFn>[0]; resolve: () => void }>;
+let tracks: RefAudio[];
+let setTracks: (next: RefAudio[]) => void;
+let setLimit: (next: number) => void;
+let setImageSupported: (next: boolean) => void;
+let images: unknown[];
+
+describe("audio prompt upload completion", () => {
+  beforeEach(async () => {
+    (globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
+    probes = [];
+    uploads = [];
+    images = [];
+    handle = createRef<AudioReferenceRowHandle>();
+    const nativeCreate = document.createElement.bind(document);
+    vi.spyOn(document, "createElement").mockImplementation((tag: string, options?: ElementCreationOptions) => {
+      const element = nativeCreate(tag, options);
+      if (tag === "audio") probes.push(element as HTMLAudioElement);
+      return element;
+    });
+    vi.stubGlobal("URL", class extends URL {
+      static createObjectURL = vi.fn(() => "blob:test");
+      static revokeObjectURL = vi.fn();
+    });
+    container = document.createElement("div");
+    document.body.append(container);
+    root = createRoot(container);
+    await act(async () => root.render(<Harness />));
+  });
+
+  afterEach(async () => {
+    await act(async () => root.unmount());
+    container.remove();
+    vi.restoreAllMocks();
+    vi.unstubAllGlobals();
+  });
+
+  it("uses current model capacity before beginning an upload after metadata", async () => {
+    const pending = handle.current!.addAudioFiles([FILE]);
+    await act(async () => setLimit(0));
+    await metadata(0, 10);
+    expect(uploads).toHaveLength(0);
+    await pending;
+    expect(tracks).toEqual([]);
+  });
+
+  it("does not append an upload when the model capacity became zero", async () => {
+    const pending = handle.current!.addAudioFiles([FILE]);
+    await metadata(0, 10);
+    await act(async () => setLimit(0));
+    await complete(0, "new");
+    await pending;
+    expect(tracks).toEqual([]);
+  });
+
+  it("enforces the current total duration at upload completion", async () => {
+    const pending = handle.current!.addAudioFiles([FILE]);
+    await metadata(0, 10);
+    await act(async () => setTracks([EXISTING]));
+    await complete(0, "new");
+    await pending;
+    expect(tracks).toEqual([EXISTING]);
+  });
+
+  it("retains both independent callbacks completing before the next render", async () => {
+    const first = handle.current!.addAudioFiles([FILE]);
+    const second = handle.current!.addAudioFiles([FILE]);
+    await metadata(0, 10);
+    await metadata(1, 20);
+    await act(async () => {
+      uploads[0].args.progressCallback({ status: UploaderStates.success, data: "first" });
+      uploads[1].args.progressCallback({ status: UploaderStates.success, data: "second" });
+      uploads[0].resolve();
+      uploads[1].resolve();
+      await Promise.all([first, second]);
+    });
+    expect(tracks.map((a) => a.mediaToken)).toEqual(["first", "second"]);
+  });
+
+  it("does not exceed a one-track limit with concurrent callbacks", async () => {
+    await act(async () => setLimit(1));
+    const first = handle.current!.addAudioFiles([FILE]);
+    const second = handle.current!.addAudioFiles([FILE]);
+    await metadata(0, 10);
+    await metadata(1, 20);
+    await act(async () => {
+      uploads[0].args.progressCallback({ status: UploaderStates.success, data: "first" });
+      uploads[1].args.progressCallback({ status: UploaderStates.success, data: "second" });
+      uploads[0].resolve();
+      uploads[1].resolve();
+      await Promise.all([first, second]);
+    });
+    expect(tracks.map((a) => a.mediaToken)).toEqual(["first"]);
+  });
+
+  it("keeps an ordinary two-file sequential upload working", async () => {
+    const pending = handle.current!.addAudioFiles([FILE, FILE]);
+    await metadata(0, 10);
+    await complete(0, "first");
+    await metadata(1, 20);
+    await complete(1, "second");
+    await pending;
+    expect(tracks.map((a) => a.mediaToken)).toEqual(["first", "second"]);
+  });
+
+  it("discards an image completion when the current model stopped supporting images", async () => {
+    let pending!: Promise<void>;
+    await act(async () => { pending = handle.current!.addImageFile(new File([], "image.png")); });
+    await act(async () => setImageSupported(false));
+    await complete(0, "image-token");
+    await pending;
+    expect(images).toEqual([]);
+  });
+
+  it("retains an ordinary image upload", async () => {
+    let pending!: Promise<void>;
+    await act(async () => { pending = handle.current!.addImageFile(new File([], "image.png")); });
+    await complete(0, "image-token");
+    await pending;
+    expect(images).toHaveLength(1);
+  });
+});
+
+function Harness() {
+  const [current, update] = useState<RefAudio[]>([]);
+  const [limit, updateLimit] = useState(3);
+  const [imageSupported, updateImageSupported] = useState(true);
+  tracks = current;
+  setTracks = update;
+  setLimit = updateLimit;
+  setImageSupported = updateImageSupported;
+  const upload: UploadMediaFn = (args) => new Promise((resolve) => uploads.push({ args, resolve }));
+  return <AudioReferenceRow ref={handle} referenceAudios={current} onReferenceAudiosChange={update}
+    maxAudioCount={limit} maxAudioRefDuration={600} uploadAudio={upload} uploadImage={upload}
+    imageSupported={imageSupported} onReferenceImagesChange={(next) => { images = next; }} />;
+}
+
+async function metadata(index: number, duration: number) {
+  await act(async () => {
+    Object.defineProperty(probes[index], "duration", { value: duration });
+    probes[index].dispatchEvent(new Event("loadedmetadata"));
+  });
+}
+
+async function complete(index: number, token: string) {
+  await act(async () => {
+    uploads[index].args.progressCallback({ status: UploaderStates.success, data: token });
+    uploads[index].resolve();
+  });
+}
diff --git a/frontend/libs/components/promptbox/src/lib/deck/useDeckMedia.spec.tsx b/frontend/libs/components/promptbox/src/lib/deck/useDeckMedia.spec.tsx
new file mode 100644
--- /dev/null
+++ b/frontend/libs/components/promptbox/src/lib/deck/useDeckMedia.spec.tsx
@@ -0,0 +1,321 @@
+import { act, useState, useLayoutEffect, type ReactElement } from "react";
+import { createRoot, type Root } from "react-dom/client";
+import { UploaderStates } from "@storyteller/common";
+import type { UploadMediaFn } from "@storyteller/api";
+import { useDeckMedia, type DeckMediaRefLike } from "./useDeckMedia";
+
+let root: Root;
+let container: HTMLDivElement;
+let deck: ReturnType<typeof useDeckMedia>;
+let references: DeckMediaRefLike[];
+let audios: DeckMediaRefLike[];
+let onRemoved: (() => void) | undefined;
+let probes: HTMLVideoElement[];
+let audioProbes: HTMLAudioElement[];
+let uploads: Array<{ args: Parameters<UploadMediaFn>[0]; resolve: () => void }>;
+const originalCreate = document.createElement.bind(document);
+
+beforeEach(() => {
+  vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
+  probes = [];
+  audioProbes = [];
+  uploads = [];
+  onRemoved = undefined;
+  vi.spyOn(document, "createElement").mockImplementation((tag, options) => {
+    const element = originalCreate(tag, options);
+    if (tag === "video") probes.push(element as HTMLVideoElement);
+    if (tag === "audio") audioProbes.push(element as HTMLAudioElement);
+    return element;
+  });
+  vi.stubGlobal("URL", class extends URL {
+    static createObjectURL = vi.fn(() => `blob:${Math.random()}`);
+    static revokeObjectURL = vi.fn();
+  });
+  container = document.createElement("div");
+  document.body.append(container);
+  root = createRoot(container);
+});
+
+afterEach(async () => {
+  await act(async () => root.unmount());
+  container.remove();
+  vi.restoreAllMocks();
+  vi.unstubAllGlobals();
+});
+
+it("does not restore a reference removed while another video uploads", async () => {
+  await render([reference("old")]);
+  let pending!: Promise<void>;
+  await act(async () => { pending = deck.processVideoFiles([file("new")]); });
+  await metadata(0);
+  await act(async () => container.querySelector("button")!.click());
+  expect(references).toEqual([]);
+  await finish(0, "new", pending);
+  expect(references.map((item) => item.mediaToken)).toEqual(["new"]);
+});
+
+it("keeps both independently completed video uploads", async () => {
+  await render([]);
+  let first!: Promise<void>;
+  let second!: Promise<void>;
+  await act(async () => {
+    first = deck.processVideoFiles([file("a")]);
+    second = deck.processVideoFiles([file("b")]);
+  });
+  await metadata(0);
+  await metadata(1);
+  await finish(1, "b", second);
+  await finish(0, "a", first);
+  expect(references.map((item) => item.mediaToken)).toEqual(["b", "a"]);
+});
+
+it("preserves ordinary sequential multi-file uploads", async () => {
+  await render([]);
+  let pending!: Promise<void>;
+  await act(async () => { pending = deck.processVideoFiles([file("a"), file("b")]); });
+  await metadata(0);
+  await finish(0, "a");
+  await metadata(1);
+  await finish(1, "b", pending);
+  expect(references.map((item) => item.mediaToken)).toEqual(["a", "b"]);
+});
+
+it("preserves an existing reference when a normal new upload finishes", async () => {
+  await render([reference("old")]);
+  let pending!: Promise<void>;
+  await act(async () => { pending = deck.processVideoFiles([file("new")]); });
+  await metadata(0);
+  await finish(0, "new", pending);
+  expect(references.map((item) => item.mediaToken)).toEqual(["old", "new"]);
+});
+
+it.each([
+  { maxVideos: 1, maxSeconds: 15 },
+  { maxVideos: 3, maxSeconds: 3 },
+])("keeps the first committed upload when parallel completion exceeds $maxVideos videos / $maxSeconds seconds", async ({ maxVideos, maxSeconds }) => {
+  await render([], maxVideos, maxSeconds);
+  let first!: Promise<void>;
+  let second!: Promise<void>;
+  await act(async () => {
+    first = deck.processVideoFiles([file("a")]);
+    second = deck.processVideoFiles([file("b")]);
+  });
+  await metadata(0);
+  await metadata(1);
+  await finish(0, "a", first);
+  await finish(1, "b", second);
+  expect(references.map((item) => item.mediaToken)).toEqual(["a"]);
+  // Both metadata probe URLs plus the rejected reference preview are released.
+  expect(URL.revokeObjectURL).toHaveBeenCalledTimes(3);
+});
+
+it("keeps a removal committed before passive effects when an upload completes", async () => {
+  await render([reference("old")]);
+  let pending!: Promise<void>;
+  await act(async () => { pending = deck.processVideoFiles([file("new")]); });
+  await metadata(0);
+  onRemoved = () => {
+    uploads[0].args.progressCallback({ status: UploaderStates.success, data: "new" });
+    uploads[0].resolve();
+  };
+  await act(async () => container.querySelector("button")!.click());
+  await pending;
+  expect(references.map((item) => item.mediaToken)).toEqual(["new"]);
+});
+
+it("does not restore references removed while a library video is probing metadata", async () => {
+  await render([reference("old")]);
+  await act(async () => deck.openGallery("video"));
+  let pending!: Promise<void>;
+  await act(async () => { pending = selectLibrary("library"); });
+  await act(async () => container.querySelector("button")!.click());
+  await metadata(0);
+  await pending;
+  expect(references.map((item) => item.mediaToken)).toEqual(["library"]);
+});
+
+it("preserves an upload completing while a library video is probing metadata", async () => {
+  await render([]);
+  let upload!: Promise<void>;
+  await act(async () => { upload = deck.processVideoFiles([file("upload")]); });
+  await metadata(0);
+  await act(async () => deck.openGallery("video"));
+  let library!: Promise<void>;
+  await act(async () => { library = selectLibrary("library"); });
+  await finish(0, "upload", upload);
+  await metadata(1);
+  await library;
+  expect(references.map((item) => item.mediaToken)).toEqual(["upload", "library"]);
+});
+
+it("uses a stricter model's current limits when an upload finishes", async () => {
+  await render([]);
+  let pending!: Promise<void>;
+  await act(async () => { pending = deck.processVideoFiles([file("new")]); });
+  await metadata(0);
+  await render([], 0);
+  await finish(0, "new", pending);
+  expect(references).toEqual([]);
+  expect(URL.revokeObjectURL).toHaveBeenCalledTimes(2);
+});
+
+it("uses current model limits after library video metadata finishes", async () => {
+  await render([]);
+  await act(async () => deck.openGallery("video"));
+  let pending!: Promise<void>;
+  await act(async () => { pending = selectLibrary("library"); });
+  await render([], 0);
+  await metadata(0);
+  await pending;
+  expect(references).toEqual([]);
+});
+
+it("preserves ordinary library video selection", async () => {
+  await render([reference("old")]);
+  await act(async () => deck.openGallery("video"));
+  let pending!: Promise<void>;
+  await act(async () => { pending = selectLibrary("library"); });
+  await metadata(0);
+  await pending;
+  expect(references.map((item) => item.mediaToken)).toEqual(["old", "library"]);
+});
+
+it("does not restore audio removed while a library selection awaits duration results", async () => {
+  await render([], 3, 15, [reference("old")]);
+  await act(async () => deck.openGallery("audio"));
+  let pending!: Promise<void>;
+  await act(async () => { pending = selectLibrary("library"); });
+  await act(async () => container.querySelectorAll("button")[1].click());
+  Object.defineProperty(audioProbes[0], "duration", { configurable: true, value: 2 });
+  await act(async () => audioProbes[0].dispatchEvent(new Event("loadedmetadata")));
+  await pending;
+  expect(audios.map((item) => item.mediaToken)).toEqual(["library"]);
+});
+
+it("keeps audio uploads completing before the next render", async () => {
+  await render([]);
+  let first!: Promise<void>;
+  let second!: Promise<void>;
+  await act(async () => {
+    first = deck.processAudioFiles([audioFile("a")]);
+    second = deck.processAudioFiles([audioFile("b")]);
+  });
+  await metadata(0, "audio");
+  await metadata(1, "audio");
+  await act(async () => {
+    uploads[1].args.progressCallback({ status: UploaderStates.success, data: "b" });
+    uploads[1].resolve();
+    uploads[0].args.progressCallback({ status: UploaderStates.success, data: "a" });
+    uploads[0].resolve();
+    await Promise.all([first, second]);
+  });
+  expect(audios.map((item) => item.mediaToken)).toEqual(["b", "a"]);
+});
+
+it("uses current model limits when an audio upload finishes", async () => {
+  await render([]);
+  let pending!: Promise<void>;
+  await act(async () => { pending = deck.processAudioFiles([audioFile("new")]); });
+  await metadata(0, "audio");
+  await render([], 0);
+  await finish(0, "new", pending);
+  expect(audios).toEqual([]);
+  expect(URL.revokeObjectURL).toHaveBeenCalledTimes(2);
+});
+
+it("preserves an audio upload completing while library audio metadata loads", async () => {
+  await render([]);
+  let upload!: Promise<void>;
+  await act(async () => { upload = deck.processAudioFiles([audioFile("upload")]); });
+  await metadata(0, "audio");
+  await act(async () => deck.openGallery("audio"));
+  let library!: Promise<void>;
+  await act(async () => { library = selectLibrary("library"); });
+  await finish(0, "upload", upload);
+  await metadata(1, "audio");
+  await library;
+  expect(audios.map((item) => item.mediaToken)).toEqual(["upload", "library"]);
+});
+
+it("preserves ordinary audio library selection with known duration", async () => {
+  await render([], 3, 15, [reference("old")]);
+  await act(async () => deck.openGallery("audio"));
+  let pending!: Promise<void>;
+  await act(async () => { pending = selectLibrary("library", 2000); await pending; });
+  expect(audios.map((item) => item.mediaToken)).toEqual(["old", "library"]);
+});
+
+it("uses current model limits when library audio metadata finishes", async () => {
+  await render([]);
+  await act(async () => deck.openGallery("audio"));
+  let pending!: Promise<void>;
+  await act(async () => { pending = selectLibrary("library"); });
+  await render([], 0);
+  await metadata(0, "audio");
+  await pending;
+  expect(audios).toEqual([]);
+});
+
+function Harness({ initial, maxVideos, maxSeconds, initialAudios }: { initial: DeckMediaRefLike[]; maxVideos: number; maxSeconds: number; initialAudios: DeckMediaRefLike[] }) {
+  const [videos, setVideos] = useState(initial);
+  references = videos;
+  const [audioRefs, setAudios] = useState(initialAudios);
+  audios = audioRefs;
+  deck = useDeckMedia({
+    referenceImages: [], setReferenceImages: () => {}, maxImages: 0,
+    referenceVideos: videos, setReferenceVideos: setVideos,
+    maxVideos, maxVideoTotalSec: maxSeconds,
+    referenceAudios: audioRefs, setReferenceAudios: setAudios,
+    maxAudios: maxVideos, maxAudioTotalSec: maxSeconds,
+    ownGalleryModal: true,
+    uploadVideo: (args) => new Promise<void>((resolve) => uploads.push({ args, resolve })),
+    uploadAudio: (args) => new Promise<void>((resolve) => uploads.push({ args, resolve })),
+  });
+  useLayoutEffect(() => {
+    if (videos.length === 0 && onRemoved) {
+      const complete = onRemoved;
+      onRemoved = undefined;
+      complete();
+    }
+  }, [videos]);
+  return <>
+    <button onClick={() => setVideos(videos.filter((video) => video.id !== "old"))}>Remove old reference</button>
+    <button onClick={() => setAudios(audioRefs.filter((audio) => audio.id !== "old"))}>Remove old audio</button>
+  </>;
+}
+
+async function render(initial: DeckMediaRefLike[], maxVideos = 3, maxSeconds = 15, initialAudios: DeckMediaRefLike[] = []) {
+  await act(async () => root.render(<Harness initial={initial} maxVideos={maxVideos} maxSeconds={maxSeconds} initialAudios={initialAudios} />));
+}
+
+function reference(id: string): DeckMediaRefLike {
+  return { id, mediaToken: id, url: `https://example.com/${id}.mp4`, duration: 2 };
+}
+
+function file(name: string) {
+  return new File([], `${name}.mp4`, { type: "video/mp4" });
+}
+
+function audioFile(name: string) {
+  return new File([], `${name}.mp3`, { type: "audio/mpeg" });
+}
+
+async function metadata(index: number, kind: "video" | "audio" = "video") {
+  const video = kind === "video" ? probes[index] : audioProbes[index];
+  Object.defineProperty(video, "duration", { configurable: true, value: 2 });
+  await act(async () => video.dispatchEvent(new Event("loadedmetadata")));
+}
+
+async function finish(index: number, token: string, pending?: Promise<void>) {
+  await act(async () => {
+    const upload = uploads[index];
+    upload.args.progressCallback({ status: UploaderStates.success, data: token });
+    upload.resolve();
+    if (pending) await pending;
+  });
+}
+
+function selectLibrary(id: string, durationMillis?: number) {
+  const modal = deck.galleryModal as ReactElement<{ onUseSelected: (items: Array<{ id: string; fullImage: string; durationMillis?: number }>) => Promise<void> }>;
+  return modal.props.onUseSelected([{ id, fullImage: `https://example.com/${id}.mp4`, durationMillis }]);
+}
diff --git a/frontend/libs/components/promptbox/src/lib/deck/useDeckMedia.tsx b/frontend/libs/components/promptbox/src/lib/deck/useDeckMedia.tsx
--- a/frontend/libs/components/promptbox/src/lib/deck/useDeckMedia.tsx
+++ b/frontend/libs/components/promptbox/src/lib/deck/useDeckMedia.tsx
@@ -1,4 +1,4 @@
-import { useEffect, useMemo, useRef, useState, type ReactNode } from "react";
+import { useEffect, useLayoutEffect, useMemo, useRef, useState, type ReactNode } from "react";
 import { GalleryItem, GalleryModal } from "@storyteller/ui-gallery-modal";
 import { type UploadMediaFn } from "@storyteller/api";
 import { downloadFileFromUrl } from "@storyteller/tauri-api";
@@ -174,10 +174,19 @@ export function useDeckMedia<
     referenceImagesRef.current = referenceImages;
   }, [referenceImages]);
 
+  const referenceVideosRef = useRef(referenceVideos);
+  const videoLimitsRef = useRef({ maxVideos, maxVideoTotalSec });
+  useLayoutEffect(() => {
+    referenceVideosRef.current = referenceVideos;
+    videoLimitsRef.current = { maxVideos, maxVideoTotalSec };
+  }, [referenceVideos, maxVideos, maxVideoTotalSec]);
+
   const referenceAudiosRef = useRef(referenceAudios);
-  useEffect(() => {
+  const audioLimitsRef = useRef({ maxAudios, maxAudioTotalSec });
+  useLayoutEffect(() => {
     referenceAudiosRef.current = referenceAudios;
-  }, [referenceAudios]);
+    audioLimitsRef.current = { maxAudios, maxAudioTotalSec };
+  }, [referenceAudios, maxAudios, maxAudioTotalSec]);
 
   // Committed refs live in the caller's store, whose update can render one
   // pass before this hook's entry-removal state lands — briefly showing the
@@ -352,9 +361,7 @@ export function useDeckMedia<
   };
 
   const processVideoFiles = async (files: File[]) => {
-    // Snapshot the committed state at call time so removes that happened
-    // before this call are respected (don't re-read a stale ref).
-    const baseVideos = [...referenceVideos];
+    const baseVideos = referenceVideosRef.current;
     const availableSlots = Math.max(0, maxVideos - baseVideos.length);
     if (availableSlots <= 0) {
       toast.error(videoLimitMessage(maxVideos, maxVideoTotalSec), {
@@ -364,14 +371,20 @@ export function useDeckMedia<
     }
 
     const filesToProcess = files.slice(0, availableSlots);
-    let committed = baseVideos;
 
     for (const file of filesToProcess) {
       const duration = await getVideoDuration(file);
-      const currentTotal = committed.reduce((sum, v) => sum + v.duration, 0);
+      const currentTotal = referenceVideosRef.current.reduce(
+        (sum, v) => sum + v.duration,
+        0,
+      );
 
-      if (currentTotal + duration > maxVideoTotalSec) {
-        toast.error(`Total video duration cannot exceed ${maxVideoTotalSec}s`, {
+      const limits = videoLimitsRef.current;
+      if (
+        referenceVideosRef.current.length >= limits.maxVideos ||
+        currentTotal + duration > limits.maxVideoTotalSec
+      ) {
+        toast.error(videoLimitMessage(limits.maxVideos, limits.maxVideoTotalSec), {
           id: "video-ref-limit",
         });
         break;
@@ -392,8 +405,23 @@ export function useDeckMedia<
           duration,
         });
         setUploadingVideo(null);
-        committed = [...committed, refVideo];
-        setReferenceVideos?.(committed);
+        const current = referenceVideosRef.current;
+        const limits = videoLimitsRef.current;
+        if (
+          current.length >= limits.maxVideos ||
+          current.reduce((sum, v) => sum + v.duration, 0) + duration > limits.maxVideoTotalSec
+        ) {
+          URL.revokeObjectURL(entry.previewUrl);
+          toast.error(videoLimitMessage(limits.maxVideos, limits.maxVideoTotalSec), {
+            id: "video-ref-limit",
+          });
+          return;
+        }
+        // Uploads can finish before another render/effect. Publish to the
+        // shared ref immediately so concurrent completions append safely.
+        const next = [...current, refVideo];
+        referenceVideosRef.current = next;
+        setReferenceVideos?.(next);
       };
 
       if (uploadVideo) {
@@ -449,8 +477,12 @@ export function useDeckMedia<
         0,
       );
 
-      if (currentTotal + duration > maxAudioTotalSec) {
-        toast.error(`Total audio duration cannot exceed ${maxAudioTotalSec}s`);
+      const limits = audioLimitsRef.current;
+      if (
+        referenceAudiosRef.current.length >= limits.maxAudios ||
+        currentTotal + duration > limits.maxAudioTotalSec
+      ) {
+        toast.error(`Max ${limits.maxAudios} audio tracks / ${limits.maxAudioTotalSec}s total`);
         break;
       }
 
@@ -466,7 +498,19 @@ export function useDeckMedia<
           duration,
         });
         setUploadingAudio(null);
-        setReferenceAudios?.([...referenceAudiosRef.current, refAudio]);
+        const current = referenceAudiosRef.current;
+        const limits = audioLimitsRef.current;
+        if (
+          current.length >= limits.maxAudios ||
+          current.reduce((sum, a) => sum + a.duration, 0) + duration > limits.maxAudioTotalSec
+        ) {
+          URL.revokeObjectURL(entry.previewUrl);
+          toast.error(`Max ${limits.maxAudios} audio tracks / ${limits.maxAudioTotalSec}s total`);
+          return;
+        }
+        const next = [...current, refAudio];
+        referenceAudiosRef.current = next;
+        setReferenceAudios?.(next);
       };
 
       if (uploadAudio) {
@@ -552,9 +596,7 @@ export function useDeckMedia<
 
   const handleGalleryImages = async (selectedItems: GalleryItem[]) => {
     if (galleryTarget === "video") {
-      // Snapshot committed state at call time to avoid re-reading a stale
-      // ref after removes.
-      const baseVideos = [...referenceVideos];
+      const baseVideos = referenceVideosRef.current;
       const availableSlots = Math.max(0, maxVideos - baseVideos.length);
       if (availableSlots <= 0) {
         toast.error(videoLimitMessage(maxVideos, maxVideoTotalSec), {
@@ -577,13 +619,18 @@ export function useDeckMedia<
           itemsToProcess.map((item) => getVideoDurationFromSrc(item.fullImage)),
         );
 
+        const current = referenceVideosRef.current;
+        const limits = videoLimitsRef.current;
         const newVideos: TVideo[] = [];
-        let currentTotal = baseVideos.reduce((sum, v) => sum + v.duration, 0);
+        let currentTotal = current.reduce((sum, v) => sum + v.duration, 0);
         let exceeded = false;
         for (let i = 0; i < itemsToProcess.length; i++) {
           const item = itemsToProcess[i]!;
           const duration = durations[i]!;
-          if (currentTotal + duration > maxVideoTotalSec) {
+          if (
+            current.length + newVideos.length >= limits.maxVideos ||
+            currentTotal + duration > limits.maxVideoTotalSec
+          ) {
             exceeded = true;
             break;
           }
@@ -600,12 +647,14 @@ export function useDeckMedia<
         }
         if (exceeded) {
           toast.error(
-            `Total video duration cannot exceed ${maxVideoTotalSec}s`,
+            videoLimitMessage(limits.maxVideos, limits.maxVideoTotalSec),
             { id: "video-ref-limit" },
           );
         }
         if (newVideos.length > 0) {
-          setReferenceVideos?.([...baseVideos, ...newVideos]);
+          const next = [...current, ...newVideos];
+          referenceVideosRef.current = next;
+          setReferenceVideos?.(next);
         }
       } finally {
         setIsProcessingGallery(false);
@@ -614,7 +663,7 @@ export function useDeckMedia<
       return;
     }
     if (galleryTarget === "audio") {
-      const baseAudios = [...referenceAudios];
+      const baseAudios = referenceAudiosRef.current;
       const availableSlots = Math.max(0, maxAudios - baseAudios.length);
       if (availableSlots <= 0) {
         toast.error(`Max ${maxAudios} audio tracks / ${maxAudioTotalSec}s total`, {
@@ -641,13 +690,18 @@ export function useDeckMedia<
           ),
         );
 
+        const current = referenceAudiosRef.current;
+        const limits = audioLimitsRef.current;
         const newAudios: TAudio[] = [];
-        let currentTotal = baseAudios.reduce((sum, a) => sum + a.duration, 0);
+        let currentTotal = current.reduce((sum, a) => sum + a.duration, 0);
         let exceeded = false;
         for (let i = 0; i < itemsToProcess.length; i++) {
           const item = itemsToProcess[i]!;
           const duration = durations[i]!;
-          if (currentTotal + duration > maxAudioTotalSec) {
+          if (
+            current.length + newAudios.length >= limits.maxAudios ||
+            currentTotal + duration > limits.maxAudioTotalSec
+          ) {
             exceeded = true;
             break;
           }
@@ -664,12 +718,14 @@ export function useDeckMedia<
         }
         if (exceeded) {
           toast.error(
-            `Total audio duration cannot exceed ${maxAudioTotalSec}s`,
+            `Max ${limits.maxAudios} audio tracks / ${limits.maxAudioTotalSec}s total`,
             { id: "audio-ref-limit" },
           );
         }
         if (newAudios.length > 0) {
-          setReferenceAudios?.([...baseAudios, ...newAudios]);
+          const next = [...current, ...newAudios];
+          referenceAudiosRef.current = next;
+          setReferenceAudios?.(next);
         }
       } finally {
         setIsProcessingGallery(false);
