heygen-com/hyperframes · error · Error

storyboard frame ${frameIndex} not found

Error message

storyboard frame ${frameIndex} not found

What it means

Thrown by setFrameField() when the 1-based frameIndex does not correspond to a frame heading in the source STORYBOARD.md. frameBounds() walks the markdown splitting headings via FRAME_HEADING_RE; if `frameBounds(lines)[frameIndex - 1]` is undefined, the requested frame does not exist in the authored storyboard.

Source

Thrown at packages/core/src/storyboard/editStoryboard.ts:91

/**
 * Set (or insert) a metadata field on the frame at `frameIndex` (1-based).
 * Replaces an existing `- key: …` line (matching any alias) in place; otherwise
 * inserts a new line right after the frame heading.
 *
 * Throws when the frame doesn't exist, so a stale/raced index (e.g. the frame
 * was deleted on disk after render) surfaces as an error instead of a silent
 * no-op the UI would report as a successful save.
 */
export function setFrameField(
  source: string,
  frameIndex: number,
  key: string,
  value: string,
  opts: { aliases?: readonly string[]; quote?: boolean } = {},
): string {
  const lines = source.split(/\r?\n/);
  const target = frameBounds(lines)[frameIndex - 1];
  if (!target) throw new Error(`storyboard frame ${frameIndex} not found`);

  const aliases = new Set([key, ...(opts.aliases ?? [])].map((k) => k.toLowerCase()));
  const formatted = formatValue(value, opts.quote ?? false);

  for (let i = target.start + 1; i < target.end; i++) {
    const match = META_LINE_RE.exec(lines[i] ?? "");
    if (match && aliases.has((match[2] ?? "").toLowerCase())) {
      lines[i] = `${match[1]}${match[2]}${match[3]}${formatted}`;
      return lines.join("\n");
    }
  }

  lines.splice(target.start + 1, 0, `- ${key}: ${formatted}`);
  return lines.join("\n");
}

/** Set the voiceover (guide) line for a frame, matching any voiceover alias. */
export function setFrameVoiceover(source: string, frameIndex: number, value: string): string {

View on GitHub (pinned to c2996c8626)

Solutions

  1. Before writing, re-read the current STORYBOARD.md and re-parse frame bounds to confirm the index still exists.
  2. Validate that `frameIndex >= 1 && frameIndex <= parsedFrameCount` before calling setFrameField.
  3. If the frame was deleted intentionally, discard the edit rather than retrying against a stale index.
  4. Refresh the editor's frame list from disk on focus/save to avoid stale-index writes.

Example fix

// before
const next = setFrameField(source, frameIndex, "status", "approved");

// after
const bounds = parseStoryboard(source); // exposes frame count
if (frameIndex < 1 || frameIndex > bounds.frames.length) {
  throw new Error(`frame ${frameIndex} out of range (1..${bounds.frames.length})`);
}
const next = setFrameField(source, frameIndex, "status", "approved");
Defensive patterns

Strategy: validation

Validate before calling

import { parseStoryboard } from "./parseStoryboard.js";

function assertFrameInRange(source: string, frameIndex: number): void {
  const { frames } = parseStoryboard(source);
  if (!Number.isInteger(frameIndex) || frameIndex < 1 || frameIndex > frames.length) {
    throw new RangeError(`frame index ${frameIndex} out of range (1..${frames.length})`);
  }
}

Type guard

function isValidFrameIndex(source: string, frameIndex: unknown): boolean {
  if (typeof frameIndex !== "number" || !Number.isInteger(frameIndex)) return false;
  const { frames } = parseStoryboard(source);
  return frameIndex >= 1 && frameIndex <= frames.length;
}

Try / catch

try {
  return setFrameField(source, frameIndex, key, value, opts);
} catch (e) {
  if (/frame .* not found/.test(String(e))) {
    const { frames } = parseStoryboard(source);
    throw new RangeError(`frame ${frameIndex} no longer exists (have 1..${frames.length}); refresh from disk`, { cause: e });
  }
  throw e;
}

Prevention

When it happens

Trigger: Calling setFrameField(source, frameIndex, key, value) with frameIndex greater than the number of frame headings, frameIndex <= 0, or frameIndex valid at authoring time but stale after the storyboard was edited/deleted on disk. Used by the storyboard frame-focus editor when persisting `voiceover`/`status` edits.

Common situations: UI editor holding a stale frame list after the underlying STORYBOARD.md changed in another tab/Git pull; passing an index from a 0-based caller into the 1-based API; deleting frames in the markdown while the editor session still references the old indices; concurrent edits where a collaborator removed the frame between read and write.


AI-assisted analysis of heygen-com/hyperframes@c2996c8626 (2026-08-12). Data as JSON: /api/errors/7ead3beebb82a708. Report an issue: GitHub.