Skip to content

fix: address race conditions, group track filtering, and defensive guards#175

Merged
walterlow merged 1 commit intomainfrom
develop
Apr 11, 2026
Merged

fix: address race conditions, group track filtering, and defensive guards#175
walterlow merged 1 commit intomainfrom
develop

Conversation

@walterlow
Copy link
Copy Markdown
Owner

@walterlow walterlow commented Apr 11, 2026

Summary

  • Prevent partial audio decode from overwriting a full buffer via functional state updater
  • Preserve existing audioSrc fallback when blob URL is unavailable in sub-comp resolution; add src/audioSrc to areGroupPropsEqual comparator
  • Abort in-flight MusicGen work and revoke blob URLs on AI panel unmount
  • Record stale media IDs for error/invalid/parse-failure proxy paths in loadExistingProxies
  • Exclude group header tracks (isGroup) from patch destinations in source monitor and source edit targeting
  • Prioritize newest proxy recommendation so new IDs survive the 200-item sanitization cap
  • Stop contextmenu propagation in lazy item context menu trigger to prevent parent menus from opening
  • Guard against NaN fps/maxFrame in in-out-points sanitization
  • Add error/abort handling to captionSingle, validate sampleIntervalSec, redact caption text from logs
  • Skip scene detection cache writes on abort to avoid persisting unverified results
  • Validate duplicate provider IDs and default provider existence in ProviderRegistry constructor
  • Fix react-refresh lint warnings in nested-media-resolution-context and ai-panel

Test plan

  • All 1382 existing tests pass (2 pre-existing failures in timeline-store-facade.test.ts unrelated to this PR)
  • TypeScript type-check clean on all changed files
  • All 6 pre-push quality checks pass (boundaries, deps-contracts, legacy-lib-imports, deps-wrapper-health, edge-budgets, lint)

Summary by CodeRabbit

  • Bug Fixes

    • Prevented unintended audio/video source overwrites and avoided downgrading low-latency audio buffers.
    • Strengthened timeline parameter validation and group-track exclusions to stop invalid patching.
    • Fixed context-menu event propagation and improved provider construction validation.
  • Improvements

    • Added abort-signal support for AI flows (music generation, captioning, scene detection).
    • More accurate rendering updates when media sources change and expanded stale-proxy cleanup.
  • Tests

    • Adjusted expected ordering for proxy recommendation tests.

@vercel
Copy link
Copy Markdown

vercel bot commented Apr 11, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
freecut Ready Ready Preview, Comment Apr 11, 2026 1:29pm

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 11, 2026

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: f1f40e4a-d30d-494f-84ba-e2e08433e83f

📥 Commits

Reviewing files that changed from the base of the PR and between 6f99373 and cfbfb67.

📒 Files selected for processing (15)
  • src/features/composition-runtime/components/composition-content.tsx
  • src/features/composition-runtime/components/custom-decoder-buffered-audio.tsx
  • src/features/composition-runtime/components/stable-video-sequence.tsx
  • src/features/composition-runtime/contexts/nested-media-resolution-context.tsx
  • src/features/editor/components/ai-panel.tsx
  • src/features/media-library/services/proxy-service.ts
  • src/features/preview/components/source-monitor.tsx
  • src/features/settings/stores/settings-store.test.ts
  • src/features/settings/stores/settings-store.ts
  • src/features/timeline/components/timeline-item/item-context-menu.tsx
  • src/features/timeline/utils/in-out-points.ts
  • src/features/timeline/utils/source-edit-targeting.ts
  • src/lib/analysis/captioning/lfm-captioning-provider.ts
  • src/lib/analysis/scene-detection.ts
  • src/shared/utils/provider-registry.ts

📝 Walkthrough

Walkthrough

Refinements across composition-runtime, audio decoding, UI cleanup, proxy management, timeline/track selection, analysis abort handling, and provider validation: fixes adjust blob URL and src resolution, prevent audio-buffer regressions, tighten group-track exclusion, add abort-safe async flows, enforce provider uniqueness, and reorder proxy-recommendation state.

Changes

Cohort / File(s) Summary
Composition runtime: media/source resolution & rendering
src/features/composition-runtime/components/composition-content.tsx, src/features/composition-runtime/components/stable-video-sequence.tsx
Preserve undefined blob lookups (no forced ''); change video src/audioSrc resolution and comparison logic; treat item src/audioSrc changes as render-relevant for group equality checks.
Composition runtime: audio buffering
src/features/composition-runtime/components/custom-decoder-buffered-audio.tsx
Use functional state update to avoid overwriting an existing audio buffer when a shorter/identical preliminary decode completes; preserve longer/same-samplerate buffers.
Nested media resolution context
src/features/composition-runtime/contexts/nested-media-resolution-context.tsx
Stop exporting the mode type; export a wrapper NestedMediaResolutionProvider component (replaces direct Provider alias).
Editor: AI music cleanup
src/features/editor/components/ai-panel.tsx
Abort in-flight MusicGen requests on unmount, null the abort ref, then revoke generated object URLs.
Media library: proxy cleanup
src/features/media-library/services/proxy-service.ts
When scanning existing OPFS proxies, collect mapped media IDs into staleProxyIds for non-ready/error cases and on per-proxy exceptions to improve cleanup.
Preview & timeline selection
src/features/preview/components/source-monitor.tsx, src/features/timeline/utils/source-edit-targeting.ts, src/features/timeline/components/timeline-item/item-context-menu.tsx
Exclude track.isGroup from patch-destination/selection logic and from usable-track checks; add e.stopPropagation() to context-menu handler before preventing default and activating.
Timeline utils
src/features/timeline/utils/in-out-points.ts
Validate fps and maxFrame inputs: use safe fallbacks when non-finite or invalid, compute bounds from validated values.
Settings: proxy recommendations
src/features/settings/stores/settings-store.ts, src/features/settings/stores/settings-store.test.ts
Prepend newly normalized mediaId when marking proxy-recommended (changes ordering); update test expectation to match new order.
Analysis: captioning & scene detection
src/lib/analysis/captioning/lfm-captioning-provider.ts, src/lib/analysis/scene-detection.ts
Add AbortSignal awareness to captioning (rejects on abort, forwards signal, robust error handling and cleanup); normalize sample-interval validation; short-circuit scene verification on abort and avoid misleading warnings when aborted.
Shared utilities: provider registry
src/shared/utils/provider-registry.ts
Fail-fast constructor validation: error when duplicate provider IDs are registered or when defaultProviderId is not present.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐰 I hopped through blobs and audio seams,

Resolved the sources of broken dreams,
I kept the buffers long and true,
Aborted noise I bid adieu,
Group tracks tucked safely out of sight. ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main changes: race conditions, group track filtering, and defensive guards across multiple files.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch develop

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/lib/analysis/scene-detection.ts (1)

148-155: ⚠️ Potential issue | 🟠 Major

Abort results can still be cached through non-throw paths.

The new catch-path guard is good, but cache writes still happen when abort does not throw (e.g., verifyWithVlm() returns early on abort and Line 171 caches it). That can persist partial/unverified cuts.

Suggested fix
-  const cacheAndReturn = (results: SceneCut[]): SceneCut[] => {
-    if (cacheKey) resultsCache.set(cacheKey, results);
+  const cacheAndReturn = (results: SceneCut[]): SceneCut[] => {
+    if (cacheKey && !signal?.aborted) resultsCache.set(cacheKey, results);
     return results;
   };
 
   if (!verificationModel || deduped.length === 0 || signal?.aborted) {
     return cacheAndReturn(deduped);
   }
@@
-    return cacheAndReturn(verified);
+    return cacheAndReturn(verified);

Also applies to: 169-177

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/analysis/scene-detection.ts` around lines 148 - 155, The current
early-return uses cacheAndReturn which writes to resultsCache even when the
operation was aborted or verification didn't run; change the logic so aborted
flows never write to the cache: update cacheAndReturn (or replace its call
sites) to check signal?.aborted and skip resultsCache.set when aborted, and
ensure the early-return path in the block that checks (!verificationModel ||
deduped.length === 0 || signal?.aborted) only returns deduped without caching
when signal?.aborted is true; also apply the same non-caching behavior to the
path after verifyWithVlm() (the code around deduped/verifyWithVlm and lines
169-177) so partial or unverified cuts are never persisted to resultsCache.
🧹 Nitpick comments (1)
src/features/timeline/utils/source-edit-targeting.ts (1)

25-32: Add explicit isGroup filtering to findFirstUnlockedTrackByKind and findNearestUnlockedTrackByKind for consistency.

Both functions filter by !track.locked and getTrackKind(track) === kind, which does exclude group tracks (since getTrackKind() returns null for groups). However, other similar functions in the file (findUnlockedTrackById line 40, canUseTrackForKind line 44) explicitly check !track.isGroup. Adding this explicit filter maintains consistency with the defensive programming pattern used elsewhere and aligns with the guideline that group tracks are containers only and should never be selected for item placement.

Affected functions

Lines 25–32 (findFirstUnlockedTrackByKind):

function findFirstUnlockedTrackByKind(
  tracks: TimelineTrack[],
  kind: TrackKind,
): TimelineTrack | null {
  return [...tracks]
    .filter((track) => !track.locked && !track.isGroup && getTrackKind(track) === kind)
    .sort((a, b) => a.order - b.order)[0] ?? null;
}

Lines 140–154 (findNearestUnlockedTrackByKind):

function findNearestUnlockedTrackByKind(
  tracks: TimelineTrack[],
  targetTrack: TimelineTrack,
  kind: TrackKind,
  direction: 'above' | 'below'
): TimelineTrack | null {
  const candidates = tracks
    .filter((track) => !track.locked && !track.isGroup && getTrackKind(track) === kind)
    .filter((track) => direction === 'above'
      ? track.order < targetTrack.order
      : track.order > targetTrack.order)
    .sort((a, b) => direction === 'above' ? b.order - a.order : a.order - b.order);

  return candidates[0] ?? null;
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/features/timeline/utils/source-edit-targeting.ts` around lines 25 - 32,
The two functions findFirstUnlockedTrackByKind and
findNearestUnlockedTrackByKind should explicitly exclude group tracks for
consistency and defensive safety: update their filter pipelines to include
!track.isGroup in addition to !track.locked and getTrackKind(track) === kind so
group container tracks are never considered as selectable targets for item
placement.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/lib/analysis/captioning/lfm-captioning-provider.ts`:
- Around line 102-107: The abort handler inside captionSingle is added
anonymously to the AbortSignal and never removed, causing listener accumulation
across repeated captionSingle calls; update captionSingle (and the similar
handler used in the second block around the other call site) to attach a named
function (e.g., onAbort) via signal.addEventListener('abort', onAbort) and
ensure cleanup removes it (signal.removeEventListener('abort', onAbort)) in the
existing cleanup function, or use { once: true } when adding the listener to
avoid stacking; make sure cleanup is invoked on both resolve and reject so the
abort listener is always removed.

---

Outside diff comments:
In `@src/lib/analysis/scene-detection.ts`:
- Around line 148-155: The current early-return uses cacheAndReturn which writes
to resultsCache even when the operation was aborted or verification didn't run;
change the logic so aborted flows never write to the cache: update
cacheAndReturn (or replace its call sites) to check signal?.aborted and skip
resultsCache.set when aborted, and ensure the early-return path in the block
that checks (!verificationModel || deduped.length === 0 || signal?.aborted) only
returns deduped without caching when signal?.aborted is true; also apply the
same non-caching behavior to the path after verifyWithVlm() (the code around
deduped/verifyWithVlm and lines 169-177) so partial or unverified cuts are never
persisted to resultsCache.

---

Nitpick comments:
In `@src/features/timeline/utils/source-edit-targeting.ts`:
- Around line 25-32: The two functions findFirstUnlockedTrackByKind and
findNearestUnlockedTrackByKind should explicitly exclude group tracks for
consistency and defensive safety: update their filter pipelines to include
!track.isGroup in addition to !track.locked and getTrackKind(track) === kind so
group container tracks are never considered as selectable targets for item
placement.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 8948ff1e-6cae-40a5-a446-22143a09069a

📥 Commits

Reviewing files that changed from the base of the PR and between a4592ea and 6f99373.

📒 Files selected for processing (15)
  • src/features/composition-runtime/components/composition-content.tsx
  • src/features/composition-runtime/components/custom-decoder-buffered-audio.tsx
  • src/features/composition-runtime/components/stable-video-sequence.tsx
  • src/features/composition-runtime/contexts/nested-media-resolution-context.tsx
  • src/features/editor/components/ai-panel.tsx
  • src/features/media-library/services/proxy-service.ts
  • src/features/preview/components/source-monitor.tsx
  • src/features/settings/stores/settings-store.test.ts
  • src/features/settings/stores/settings-store.ts
  • src/features/timeline/components/timeline-item/item-context-menu.tsx
  • src/features/timeline/utils/in-out-points.ts
  • src/features/timeline/utils/source-edit-targeting.ts
  • src/lib/analysis/captioning/lfm-captioning-provider.ts
  • src/lib/analysis/scene-detection.ts
  • src/shared/utils/provider-registry.ts

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Apr 11, 2026

Greptile Summary

This PR is a multi-fix defensive hardening pass: race-condition fixes in audio buffering and sub-comp URL resolution, cleanup/abort improvements in the AI panel and captioning pipeline, group-track exclusion from patch destination logic, NaN guards in in/out-point sanitization, a new ProviderRegistry constructor guard, and a contextmenu propagation stop.

The fixes are generally sound. One incomplete fix stands out: the scene-detection cache skips writes when abort occurs during VLM verification, but still caches partial/unverified results when the signal is already aborted before the verification step begins (line 153).

Confidence Score: 4/5

Safe to merge after addressing the scene-detection partial-result cache bug; remaining findings are style/cleanup.

One P1 finding: the abort-before-verification path in scene-detection.ts still caches partial/unverified results, directly contradicting the stated intent of the fix. This causes stale data to be returned in the same session on retry. All other findings are P2. The core race-condition and filtering fixes are correct and well-implemented.

src/lib/analysis/scene-detection.ts — line 153 branch caches partial results when signal is already aborted before VLM verification begins.

Important Files Changed

Filename Overview
src/lib/analysis/scene-detection.ts Abort-case cache skip is incomplete: the catch-block fix correctly avoids caching during VLM verification abort, but cacheAndReturn(deduped) at line 153 still executes when the signal is already aborted before verification, persisting partial/unverified optical-flow results.
src/features/composition-runtime/components/custom-decoder-buffered-audio.tsx Functional state updater correctly prevents partial decode from downgrading a full buffer; race-condition fix is sound.
src/features/composition-runtime/components/stable-video-sequence.tsx Adds src and audioSrc to areGroupPropsEqual whitelist so media URL changes correctly trigger re-renders; straightforward and correct.
src/features/composition-runtime/components/composition-content.tsx Preserves existing audioSrc as fallback when blob URL is unavailable in sub-comp resolution; logic is correct.
src/features/editor/components/ai-panel.tsx Unmount cleanup correctly aborts in-flight MusicGen and revokes tracked blob URLs; saved generation URLs are properly excluded before unmount.
src/features/media-library/services/proxy-service.ts Stale media IDs are now recorded for error/invalid/parse-failure paths in loadExistingProxies; proxy recommendation ordering correctly prepends new IDs to survive the 200-item cap.
src/features/preview/components/source-monitor.tsx Group header tracks correctly excluded from patch destination picker via track.isGroup check in isPatchDestinationTrack.
src/features/timeline/utils/source-edit-targeting.ts Group tracks excluded from preferred/fallback track lookups via findUnlockedTrackById and canUseTrackForKind; activeTrack is still fetched without isGroup filtering and can become the creation reference track, though it is correctly rejected from patch destinations.
src/features/timeline/utils/in-out-points.ts NaN/infinite guards added for both fps (in getEffectiveTimelineMaxFrame) and maxFrame (in sanitizeInOutPoints); logic is correct.
src/lib/analysis/captioning/lfm-captioning-provider.ts Abort/error handling added to captionSingle; sampleIntervalSec validated; caption text correctly redacted from logs. Minor: the abort signal listener is not removed by cleanup(), leaving a dangling listener if the promise settles via onError before the signal fires.
src/shared/utils/provider-registry.ts Validates duplicate provider IDs and default provider existence in constructor; clean and well-structured.
src/features/timeline/components/timeline-item/item-context-menu.tsx Added stopPropagation on contextmenu in the lazy trigger to prevent parent menus from opening; correct fix.
src/features/settings/stores/settings-store.ts Prepends new proxy recommendation IDs so they survive the 200-item sanitization cap; correct and tested.
src/features/composition-runtime/contexts/nested-media-resolution-context.tsx React-refresh lint fix; adds eslint-disable comment for non-component exports, no functional change.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[detectScenes called] --> B{Cache hit?}
    B -- Yes --> C[Return cached results]
    B -- No --> D[Run detection pass\nhistogram / optical-flow]
    D --> E{signal.aborted?}
    E -- Yes\n[CURRENT CODE] --> F[cacheAndReturn deduped\n⚠️ caches partial results]
    E -- Yes\n[CORRECT FIX] --> G[return deduped\nno cache write]
    E -- No --> H{verificationModel set\n& deduped.length gt 0?}
    H -- No --> I[cacheAndReturn deduped]
    H -- Yes --> J[VLM verification pass]
    J --> K{Abort during verify?}
    K -- Yes --> L[return deduped\nno cache write ✅ PR fix]
    K -- No / success --> M[cacheAndReturn verified]
    K -- Error / non-abort --> N[cacheAndReturn deduped]
Loading

Comments Outside Diff (2)

  1. src/lib/analysis/scene-detection.ts, line 153-155 (link)

    P1 Abort still caches partial/unverified results

    When signal?.aborted is true here — i.e. the abort happened during the optical-flow or histogram pass, before VLM verification ever begins — cacheAndReturn(deduped) persists incomplete, unverified cuts. On the next call in the same session the cache returns those stale partial results instead of running a fresh analysis.

    The new catch block (lines 174–176) correctly skips caching when abort fires during verification, but this earlier exit path is not covered.

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: src/lib/analysis/scene-detection.ts
    Line: 153-155
    
    Comment:
    **Abort still caches partial/unverified results**
    
    When `signal?.aborted` is true here — i.e. the abort happened *during* the optical-flow or histogram pass, before VLM verification ever begins — `cacheAndReturn(deduped)` persists incomplete, unverified cuts. On the next call in the same session the cache returns those stale partial results instead of running a fresh analysis.
    
    The new `catch` block (lines 174–176) correctly skips caching when abort fires *during* verification, but this earlier exit path is not covered.
    
    
    
    How can I resolve this? If you propose a fix, please make it concise.

    Fix in Claude Code Fix in Codex

  2. src/features/timeline/utils/source-edit-targeting.ts, line 239-245 (link)

    P2 activeTrack can be a group header, becoming the creation reference

    tracks.find() here has no isGroup guard, so if the user's active track is a group header, activeTrack is non-null and becomes referenceTrack. Group tracks are correctly rejected from actual patch destinations (via canUseTrackForKind), but a group track can still reach ensureTrackForKind as the creationReferenceTrack, causing a newly created track to be inserted adjacent to a group header rather than the nearest content track. Consider filtering it out at the source:

    Prompt To Fix With AI
    This is a comment left during a code review.
    Path: src/features/timeline/utils/source-edit-targeting.ts
    Line: 239-245
    
    Comment:
    **`activeTrack` can be a group header, becoming the creation reference**
    
    `tracks.find()` here has no `isGroup` guard, so if the user's active track is a group header, `activeTrack` is non-null and becomes `referenceTrack`. Group tracks are correctly rejected from actual patch destinations (via `canUseTrackForKind`), but a group track can still reach `ensureTrackForKind` as the `creationReferenceTrack`, causing a newly created track to be inserted adjacent to a group header rather than the nearest content track. Consider filtering it out at the source:
    
    
    
    How can I resolve this? If you propose a fix, please make it concise.

    Fix in Claude Code Fix in Codex

Fix All in Claude Code Fix All in Codex

Prompt To Fix All With AI
This is a comment left during a code review.
Path: src/lib/analysis/scene-detection.ts
Line: 153-155

Comment:
**Abort still caches partial/unverified results**

When `signal?.aborted` is true here — i.e. the abort happened *during* the optical-flow or histogram pass, before VLM verification ever begins — `cacheAndReturn(deduped)` persists incomplete, unverified cuts. On the next call in the same session the cache returns those stale partial results instead of running a fresh analysis.

The new `catch` block (lines 174–176) correctly skips caching when abort fires *during* verification, but this earlier exit path is not covered.

```suggestion
  if (signal?.aborted) {
    return deduped; // Don't cache partial/unverified results from an aborted run
  }
  if (!verificationModel || deduped.length === 0) {
    return cacheAndReturn(deduped);
  }
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: src/lib/analysis/captioning/lfm-captioning-provider.ts
Line: 102-135

Comment:
**Abort listener not removed by `cleanup()`**

`cleanup()` removes the worker's `message` and `error` listeners, but the `signal.addEventListener('abort', …)` listener is left on the signal. If the worker fires an `error` event (`onError → cleanup() → reject()`), the abort listener still holds a live closure and could call `reject(signal.reason)` a second time (harmless per Promise spec, but suboptimal). The idiomatic fix is to include the abort handler in `cleanup`:

```suggestion
function captionSingle(worker: Worker, id: number, imageBlob: Blob, signal?: AbortSignal): Promise<string> {
  return new Promise<string>((resolve, reject) => {
    const onAbort = () => {
      cleanup();
      reject(signal!.reason);
    };

    const cleanup = () => {
      worker.removeEventListener('message', onMessage);
      worker.removeEventListener('error', onError);
      signal?.removeEventListener('abort', onAbort);
    };

    const onMessage = (event: MessageEvent) => {
      if (event.data.type === 'caption' && event.data.id === id) {
        cleanup();
        resolve(event.data.caption ?? '');
      }
    };

    const onError = (event: ErrorEvent) => {
      cleanup();
      reject(new Error(event.message || 'Caption worker error'));
    };

    if (signal?.aborted) {
      reject(signal.reason);
      return;
    }

    signal?.addEventListener('abort', onAbort, { once: true });

    worker.addEventListener('message', onMessage);
    worker.addEventListener('error', onError);
    worker.postMessage({ type: 'describe', id, image: imageBlob });
  });
}
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: src/features/timeline/utils/source-edit-targeting.ts
Line: 239-245

Comment:
**`activeTrack` can be a group header, becoming the creation reference**

`tracks.find()` here has no `isGroup` guard, so if the user's active track is a group header, `activeTrack` is non-null and becomes `referenceTrack`. Group tracks are correctly rejected from actual patch destinations (via `canUseTrackForKind`), but a group track can still reach `ensureTrackForKind` as the `creationReferenceTrack`, causing a newly created track to be inserted adjacent to a group header rather than the nearest content track. Consider filtering it out at the source:

```suggestion
  const activeTrack = activeTrackId
    ? (tracks.find((track) => track.id === activeTrackId && !track.isGroup) ?? null)
    : null;
```

How can I resolve this? If you propose a fix, please make it concise.

Reviews (1): Last reviewed commit: "fix: address race conditions, group trac..." | Re-trigger Greptile

Comment on lines +102 to 135
function captionSingle(worker: Worker, id: number, imageBlob: Blob, signal?: AbortSignal): Promise<string> {
return new Promise<string>((resolve, reject) => {
const cleanup = () => {
worker.removeEventListener('message', onMessage);
worker.removeEventListener('error', onError);
};

const onMessage = (event: MessageEvent) => {
if (event.data.type === 'caption' && event.data.id === id) {
worker.removeEventListener('message', onMessage);
cleanup();
resolve(event.data.caption ?? '');
}
};

const onError = (event: ErrorEvent) => {
cleanup();
reject(new Error(event.message || 'Caption worker error'));
};

if (signal?.aborted) {
reject(signal.reason);
return;
}

signal?.addEventListener('abort', () => {
cleanup();
reject(signal.reason);
}, { once: true });

worker.addEventListener('message', onMessage);
worker.addEventListener('error', onError);
worker.postMessage({ type: 'describe', id, image: imageBlob });
});
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Abort listener not removed by cleanup()

cleanup() removes the worker's message and error listeners, but the signal.addEventListener('abort', …) listener is left on the signal. If the worker fires an error event (onError → cleanup() → reject()), the abort listener still holds a live closure and could call reject(signal.reason) a second time (harmless per Promise spec, but suboptimal). The idiomatic fix is to include the abort handler in cleanup:

Suggested change
function captionSingle(worker: Worker, id: number, imageBlob: Blob, signal?: AbortSignal): Promise<string> {
return new Promise<string>((resolve, reject) => {
const cleanup = () => {
worker.removeEventListener('message', onMessage);
worker.removeEventListener('error', onError);
};
const onMessage = (event: MessageEvent) => {
if (event.data.type === 'caption' && event.data.id === id) {
worker.removeEventListener('message', onMessage);
cleanup();
resolve(event.data.caption ?? '');
}
};
const onError = (event: ErrorEvent) => {
cleanup();
reject(new Error(event.message || 'Caption worker error'));
};
if (signal?.aborted) {
reject(signal.reason);
return;
}
signal?.addEventListener('abort', () => {
cleanup();
reject(signal.reason);
}, { once: true });
worker.addEventListener('message', onMessage);
worker.addEventListener('error', onError);
worker.postMessage({ type: 'describe', id, image: imageBlob });
});
}
function captionSingle(worker: Worker, id: number, imageBlob: Blob, signal?: AbortSignal): Promise<string> {
return new Promise<string>((resolve, reject) => {
const onAbort = () => {
cleanup();
reject(signal!.reason);
};
const cleanup = () => {
worker.removeEventListener('message', onMessage);
worker.removeEventListener('error', onError);
signal?.removeEventListener('abort', onAbort);
};
const onMessage = (event: MessageEvent) => {
if (event.data.type === 'caption' && event.data.id === id) {
cleanup();
resolve(event.data.caption ?? '');
}
};
const onError = (event: ErrorEvent) => {
cleanup();
reject(new Error(event.message || 'Caption worker error'));
};
if (signal?.aborted) {
reject(signal.reason);
return;
}
signal?.addEventListener('abort', onAbort, { once: true });
worker.addEventListener('message', onMessage);
worker.addEventListener('error', onError);
worker.postMessage({ type: 'describe', id, image: imageBlob });
});
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/lib/analysis/captioning/lfm-captioning-provider.ts
Line: 102-135

Comment:
**Abort listener not removed by `cleanup()`**

`cleanup()` removes the worker's `message` and `error` listeners, but the `signal.addEventListener('abort', …)` listener is left on the signal. If the worker fires an `error` event (`onError → cleanup() → reject()`), the abort listener still holds a live closure and could call `reject(signal.reason)` a second time (harmless per Promise spec, but suboptimal). The idiomatic fix is to include the abort handler in `cleanup`:

```suggestion
function captionSingle(worker: Worker, id: number, imageBlob: Blob, signal?: AbortSignal): Promise<string> {
  return new Promise<string>((resolve, reject) => {
    const onAbort = () => {
      cleanup();
      reject(signal!.reason);
    };

    const cleanup = () => {
      worker.removeEventListener('message', onMessage);
      worker.removeEventListener('error', onError);
      signal?.removeEventListener('abort', onAbort);
    };

    const onMessage = (event: MessageEvent) => {
      if (event.data.type === 'caption' && event.data.id === id) {
        cleanup();
        resolve(event.data.caption ?? '');
      }
    };

    const onError = (event: ErrorEvent) => {
      cleanup();
      reject(new Error(event.message || 'Caption worker error'));
    };

    if (signal?.aborted) {
      reject(signal.reason);
      return;
    }

    signal?.addEventListener('abort', onAbort, { once: true });

    worker.addEventListener('message', onMessage);
    worker.addEventListener('error', onError);
    worker.postMessage({ type: 'describe', id, image: imageBlob });
  });
}
```

How can I resolve this? If you propose a fix, please make it concise.

Fix in Claude Code Fix in Codex

…ards

- Prevent partial audio decode from downgrading a full buffer in custom-decoder-buffered-audio
- Preserve existing audioSrc when blob URL is unavailable in sub-comp resolution
- Add src/audioSrc to areGroupPropsEqual so media URL changes trigger re-renders
- Abort in-flight MusicGen work and revoke blob URLs on ai-panel unmount
- Record stale media IDs for error/invalid proxy paths in loadExistingProxies
- Exclude group header tracks from patch destination and source edit targeting
- Prioritize newest proxy recommendation so new IDs survive the 200-item cap
- Stop contextmenu propagation in lazy item context menu trigger
- Guard against NaN fps/maxFrame in in-out-points sanitization
- Add error/abort handling to captionSingle, validate sampleIntervalSec, redact caption text from logs
- Skip scene detection cache writes on abort to avoid caching unverified results
- Validate duplicate provider IDs and default provider existence in ProviderRegistry
- Fix react-refresh lint warnings in nested-media-resolution-context and ai-panel
@walterlow walterlow merged commit 1875cc5 into main Apr 11, 2026
2 of 4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant