Skip to content

Editing a queued message can lock the desktop renderer at 100% CPU and >10 GiB RSS #2401

Description

@Danielalnajjar

Summary

This same failure has now occurred four times. During the latest directly observed occurrence, while an agent turn was active in bb Desktop, I clicked Edit on a queued message. The desktop UI immediately stopped responding while one Electron renderer consumed about 99% CPU and grew beyond 10 GiB RSS; the bb server and an Air/Connect client remained responsive. I expected the queued-message inline editor to open without affecting the rest of the app.

Versions and environment

  • bb 0.39.0 desktop app; bb settings version --json reported source: npm, latestVersion: 0.39.0, and no update available.
  • Packaged release git head: b33abbff098ac4c857578e7350d492dcaa65d489.
  • macOS 26.6.2 (25G83) on the Pro that runs the sole bb server and desktop renderer.
  • Codex CLI 0.149.1; a Codex turn was active, but the provider process did not appear to be the failing boundary.
  • The same thread was visible remotely through bb Connect on the Air. The remote client and server remained healthy while only the Pro's Electron renderer was locked.
  • Project proj_ivbg7bbvgw; environment env_4brjp4yccv; thread thr_n9sh8rhz6t.

Steps to reproduce

The reporter has now experienced this failure four times; the latest occurrence was directly observed on the latest release. I did not deliberately repeat it after recovery because the affected renderer had already exceeded 10 GiB RSS and was still growing.

  1. Open bb Desktop with an agent turn active in a thread.
  2. Queue a follow-up message so it appears in the queued-messages area.
  3. Click Edit on that queued message.
  4. Observe that the desktop renderer stops responding.

Observed recovery attempts:

  • Ordinary reload (Cmd+R) did not recover the UI.
  • Terminating only the affected renderer left the bb server and Air client healthy.
  • Force Reload recovered the desktop UI.

The exact queued-message text was not isolated as a contributing factor. A controlled second reproduction has not been attempted.

Expected vs actual

Expected:
The queued-message inline editor opens and bb Desktop remains responsive.

Actual:
- A single `bb Helper (Renderer)` process, PID 55035, stayed at about 99.3-99.4% CPU.
- Its RSS grew past 10 GiB during observation and continued increasing.
- The bb server process and Air/Connect client remained responsive.
- Cmd+R did not recover the renderer; Force Reload ultimately did.
- No persisted-data loss was observed.

Evidence

  • CPU and RSS were measured with ps against renderer PID 55035 while the UI was frozen.
  • Two macOS process samples were captured before recovery:
    • /tmp/bb-renderer-hang.txt — 631,364 bytes, captured 2026-08-25 04:02 PDT.
    • /tmp/bb-renderer-55035.sample — 318,664 bytes, captured 2026-08-25 04:23 PDT.
  • Both samples identify bb Helper (Renderer) version 0.39.0. Their dominant main-thread stack is inside Electron/V8 microtask execution. That localizes the hot work to the renderer but does not name the application callback responsible.
  • Investigation thread: https://dan.getbb.app/projects/proj_ivbg7bbvgw/threads/thr_n9sh8rhz6t

Suspected cause, not yet proven

The strongest source-level lead is a resize/animation/scroll feedback loop in the queued-message inline editor:

  • The installed release observes the viewport, editor surface, list, editor, composer shell, and container with one callback that measures and updates editor height, then realigns scroll:
    const measureInlineEditorMaxHeight = useCallback(() => {
    if (!inlineEditorActive) {
    setInlineEditorMaxHeight(null);
    setInlineEditorDesiredHeight(null);
    return;
    }
    const viewport = getScrollElement?.();
    const surface = surfaceRef.current;
    const composerShell = surface?.closest<HTMLElement>("[data-app-composer]");
    const container = composerShell?.parentElement;
    if (!viewport || !surface || !container) {
    setInlineEditorMaxHeight(null);
    return;
    }
    const nextHeight = getInlineEditorSurfaceMaxHeight({
    containerHeight: container.getBoundingClientRect().height,
    surfaceHeight: surface.getBoundingClientRect().height,
    viewportHeight: viewport.clientHeight,
    });
    setInlineEditorMaxHeight((currentHeight) =>
    currentHeight === nextHeight ? currentHeight : nextHeight,
    );
    const list = listRef.current;
    const scroll = scrollRef.current;
    const editorElement = list?.querySelector<HTMLElement>(
    "[data-queued-message-inline-editor]",
    );
    if (!list || !scroll || !editorElement) {
    setInlineEditorDesiredHeight(null);
    return;
    }
    const items = Array.from(list.children);
    const editorIndex = items.indexOf(editorElement);
    const previousRow = items
    .slice(0, editorIndex)
    .reverse()
    .find((item) => item.hasAttribute("data-queued-message-row"));
    const followingRow = items
    .slice(editorIndex + 1)
    .find((item) => item.hasAttribute("data-queued-message-row"));
    const firstElement = (previousRow ?? editorElement) as HTMLElement;
    const lastElement = (followingRow ?? editorElement) as HTMLElement;
    const surfaceRect = surface.getBoundingClientRect();
    const scrollRect = scroll.getBoundingClientRect();
    const contentHeight =
    lastElement.getBoundingClientRect().bottom -
    firstElement.getBoundingClientRect().top;
    const chromeHeight = Math.max(0, surfaceRect.height - scrollRect.height);
    const desiredHeight = Math.max(
    WORKSPACE_MIN_HEIGHT,
    Math.ceil(contentHeight + chromeHeight),
    );
    setInlineEditorDesiredHeight((currentHeight) =>
    currentHeight === desiredHeight ? currentHeight : desiredHeight,
    );
    // The surface height is animated. ResizeObserver calls this throughout the
    // transition, so re-align the neighborhood as usable space appears instead
    // of leaving the editor pinned to the top based on the first, short frame.
    scrollInlineEditorNeighborhoodIntoView();
    }, [
    getScrollElement,
    inlineEditorActive,
    scrollInlineEditorNeighborhoodIntoView,
    scrollRef,
    ]);
    useLayoutEffect(() => {
    measureInlineEditorMaxHeight();
    if (!inlineEditorActive) return;
    const viewport = getScrollElement?.();
    const surface = surfaceRef.current;
    const composerShell = surface?.closest<HTMLElement>("[data-app-composer]");
    const container = composerShell?.parentElement;
    const animationFrame = window.requestAnimationFrame(
    measureInlineEditorMaxHeight,
    );
    const resizeObserver =
    typeof ResizeObserver === "undefined"
    ? null
    : new ResizeObserver(measureInlineEditorMaxHeight);
    if (viewport) resizeObserver?.observe(viewport);
    if (surface) resizeObserver?.observe(surface);
    if (listRef.current) resizeObserver?.observe(listRef.current);
    const editorElement = listRef.current?.querySelector<HTMLElement>(
    "[data-queued-message-inline-editor]",
    );
    if (editorElement) resizeObserver?.observe(editorElement);
    if (composerShell) resizeObserver?.observe(composerShell);
    if (container) resizeObserver?.observe(container);
    window.addEventListener("resize", measureInlineEditorMaxHeight);
    return () => {
    window.cancelAnimationFrame(animationFrame);
    resizeObserver?.disconnect();
    window.removeEventListener("resize", measureInlineEditorMaxHeight);
    };
    }, [getScrollElement, inlineEditorActive, measureInlineEditorMaxHeight]);
  • The editor surface animates its height, so those measurements can be re-entered throughout the transition:
    <PromptStackCard
    rootRef={surfaceRef}
    ariaLabel="Queued messages"
    style={{ height: surfaceHeight }}
    className={cn(
    "relative z-10 flex min-h-0 flex-col overflow-hidden bg-surface-raised-solid shadow-lift",
    inlineEditor
    ? "mb-0 rounded-xl pb-4"
    : "-mb-5 rounded-xl rounded-b-none border-b-0 pb-3",
    !surfaceDragging &&
    "transition-[height,margin,border-radius,padding] duration-[260ms] ease-[cubic-bezier(0.16,1,0.3,1)] motion-reduce:transition-none",
  • The same measurement structure remains on current main at 42d2c1bec2e9830bd8e2b8f5f355ebffda37bded:
    const measureInlineEditorMaxHeight = useCallback(() => {
    if (!inlineEditorActive) {
    setInlineEditorMaxHeight(null);
    setInlineEditorDesiredHeight(null);
    return;
    }
    const viewport = getScrollElement?.();
    const surface = surfaceRef.current;
    const composerShell = surface?.closest<HTMLElement>("[data-app-composer]");
    const container = composerShell?.parentElement;
    if (!viewport || !surface || !container) {
    setInlineEditorMaxHeight(null);
    return;
    }
    const nextHeight = getInlineEditorSurfaceMaxHeight({
    containerHeight: container.getBoundingClientRect().height,
    surfaceHeight: surface.getBoundingClientRect().height,
    viewportHeight: viewport.clientHeight,
    });
    setInlineEditorMaxHeight((currentHeight) =>
    currentHeight === nextHeight ? currentHeight : nextHeight,
    );
    const list = listRef.current;
    const scroll = scrollRef.current;
    const editorElement = list?.querySelector<HTMLElement>(
    "[data-queued-message-inline-editor]",
    );
    if (!list || !scroll || !editorElement) {
    setInlineEditorDesiredHeight(null);
    return;
    }
    const items = Array.from(list.children);
    const editorIndex = items.indexOf(editorElement);
    const previousRow = items
    .slice(0, editorIndex)
    .reverse()
    .find((item) => item.hasAttribute("data-queued-message-row"));
    const followingRow = items
    .slice(editorIndex + 1)
    .find((item) => item.hasAttribute("data-queued-message-row"));
    const firstElement = (previousRow ?? editorElement) as HTMLElement;
    const lastElement = (followingRow ?? editorElement) as HTMLElement;
    const surfaceRect = surface.getBoundingClientRect();
    const scrollRect = scroll.getBoundingClientRect();
    const contentHeight =
    lastElement.getBoundingClientRect().bottom -
    firstElement.getBoundingClientRect().top;
    const chromeHeight = Math.max(0, surfaceRect.height - scrollRect.height);
    const desiredHeight = Math.max(
    WORKSPACE_MIN_HEIGHT,
    Math.ceil(contentHeight + chromeHeight),
    );
    setInlineEditorDesiredHeight((currentHeight) =>
    currentHeight === desiredHeight ? currentHeight : desiredHeight,
    );
    // The surface height is animated. ResizeObserver calls this throughout the
    // transition, so re-align the neighborhood as usable space appears instead
    // of leaving the editor pinned to the top based on the first, short frame.
    scrollInlineEditorNeighborhoodIntoView();
    }, [
    getScrollElement,
    inlineEditorActive,
    scrollInlineEditorNeighborhoodIntoView,
    scrollRef,
    ]);
    useLayoutEffect(() => {
    measureInlineEditorMaxHeight();
    if (!inlineEditorActive) return;
    const viewport = getScrollElement?.();
    const surface = surfaceRef.current;
    const composerShell = surface?.closest<HTMLElement>("[data-app-composer]");
    const container = composerShell?.parentElement;
    const animationFrame = window.requestAnimationFrame(
    measureInlineEditorMaxHeight,
    );
    const resizeObserver =
    typeof ResizeObserver === "undefined"
    ? null
    : new ResizeObserver(measureInlineEditorMaxHeight);
    if (viewport) resizeObserver?.observe(viewport);
    if (surface) resizeObserver?.observe(surface);
    if (listRef.current) resizeObserver?.observe(listRef.current);
    const editorElement = listRef.current?.querySelector<HTMLElement>(
    "[data-queued-message-inline-editor]",
    );
    if (editorElement) resizeObserver?.observe(editorElement);
    if (composerShell) resizeObserver?.observe(composerShell);
    if (container) resizeObserver?.observe(container);
    window.addEventListener("resize", measureInlineEditorMaxHeight);
    return () => {
    window.cancelAnimationFrame(animationFrame);
    resizeObserver?.disconnect();
    window.removeEventListener("resize", measureInlineEditorMaxHeight);
    };
    }, [getScrollElement, inlineEditorActive, measureInlineEditorMaxHeight]);

This mechanism is a strong inference from the trigger, process sample, and source. The process sample does not directly attribute the loop to measureInlineEditorMaxHeight, so the report should not present it as a confirmed root cause.

What you ruled out

Suggested priority and effort

High — this is now a recurring failure with four reported occurrences. One common queued-message action can make the local desktop unusable and drive unbounded renderer memory growth. Force Reload is a workaround and no persisted-data loss was observed. Likely effort: Medium, beginning with a bounded regression test or instrumentation around queued-editor resize convergence before choosing a fix.

Checks

  • The reporter experienced this four times; the latest occurrence was directly observed on the latest release. I explicitly state why I did not deliberately trigger the runaway again during this investigation.
  • I searched open and closed issues and PRs for the same problem.
  • This report separates the observed failure from the suspected source-level mechanism.
  • The body links the investigation thread and ends with the required marker.

BB-Thread-ID: thr_n9sh8rhz6t

AGENT GENERATED: by GPT-5.6-Sol

Metadata

Metadata

Assignees

No one assigned

    Labels

    desktopDesktop app, install, update, packaginguiApp shell, sidebar, composer, rendering

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions