← reports

#7219 · Legacy update responses crash Settings

BugHigh priorityMedium effortuiGitHub issueNovember 7, 2026

Verdict: REPRODUCED · Root-cause confidence: high · Reproduction label: confirmed-repro

Trusted main base: 88c78686805e828dda9f8d61fd5cd29aca26c138.

1. TL;DR

The current Settings update row crashes when an older server returns update status without scheduling fields. The row checks that scheduled is not null, but an absent field is undefined, which passes that check. It then reads requiredIdleMs from undefined and throws the exact reported exception. A focused test against unchanged main reproduced this twice, including once in a second clean checkout. This verifies the rendering defect and legacy-response trigger, not the reporter's exact installed client/server combination or the claim that every thread was affected.

2. Claims vs findings

ClaimFindingEvidence
Settings crashes while reading requiredIdleMs.VerifiedBoth trusted-main tests throw at InAppUpdateRow, UpdatesSettingsSection.tsx:874:28.
The installation includes an older release.Supported mechanism; installation unverifiedThe 0.48.0 source contract has no scheduling fields. No installed runtime or user data was inspected.
Running desktop and mobile is sufficient.UnverifiedThe reproducer supplies a legacy SDK response to the real Settings component. Neither native application was launched.
Every thread loses its content.UnverifiedThe test establishes a Settings render crash only; it does not establish a thread data-loss or navigation defect.

3. Environment

4. Minimal reproduction

  1. Create a clean checkout from the trusted repository, and select the recorded main commit:
    git clone https://github.com/get-bb/bb.git issue-7219-repro
    cd issue-7219-repro
    git checkout --detach 88c78686805e828dda9f8d61fd5cd29aca26c138
    pnpm install --frozen-lockfile --prefer-offline
  2. In apps/app/src/components/settings/UpdatesSettingsSection.test.tsx, insert the following test immediately inside the existing describe("UpdatesSettingsSection", ...). It reuses the file's existing helpers and models the fields absent from the legacy server response; it does not replace the production component.
    it("renders an update from a server without update scheduling fields", async () => {
      useWebApp();
      const legacyStatus = makeAppUpdateStatus();
      Reflect.deleteProperty(legacyStatus, "scheduled");
      Reflect.deleteProperty(legacyStatus, "autoUpdate");
      Reflect.deleteProperty(legacyStatus, "availableSkipped");
      vi.mocked(sdk.system.appUpdate).mockResolvedValue(legacyStatus);
    
      renderSection();
    
      expect(
        await screen.findByRole("button", {
          name: "Update available · Download the update and restart bb",
        }),
      ).toBeDefined();
      expect(screen.queryByText(/updating shortly/i)).toBeNull();
    });
  3. Run the authored test through Turbo:
    pnpm exec turbo run test --filter=@bb/app -- src/components/settings/UpdatesSettingsSection.test.tsx -t 'renders an update from a server without update scheduling fields'

Expected: the ordinary Update control renders, with no scheduled-update caption or uncaught exception.

Actual, before any production change, in both checkouts (verbatim diagnostic excerpt):

TypeError: Cannot read properties of undefined (reading 'requiredIdleMs')
 ❯ InAppUpdateRow src/components/settings/UpdatesSettingsSection.tsx:874:28

 Test Files  1 failed (1)
      Tests  1 failed | 75 skipped (76)
     Errors  1 error

The failed assertion cannot find the Update button because React has already thrown during rendering. This is a nonvisual exception regression; no screenshot is offered as evidence. The complete added test is inline rather than a separately committed script, following the reports repository's artifact policy.

5. Root cause

The current update row assumes a present nullable field:

if (status.scheduled !== null) {
  ...
  scheduledAppUpdateCaption(
    status.runningThreadCount,
    status.scheduled.requiredIdleMs,
  )
}

For a legacy response, undefined !== null is true. The branch therefore dereferences an absent object. This is independent of whether update-when-idle is enabled: the row performs the check whenever it renders idle update status.

The SDK appUpdate implementation forwards decoded JSON without normalizing old response shapes. Its TypeScript return type does not add missing JSON fields. The current contract requires scheduling metadata, but that is not a runtime guarantee for an older server.

Historical supporting evidence was read, not executed: the 0.48.0 contract has no scheduled, autoUpdate, or availableSkipped. Scheduling was added in b4b1e4a85e. Mixed client/server versions can therefore expose a genuine previously shipped payload, rather than an arbitrary malformed fixture. The report does not claim proof of which version pairing the reporter was running.

6. Proposed fix and verification

Treat both null and an absent scheduling object as no scheduled update at this rendering compatibility boundary:

- if (status.scheduled !== null) {
+ if (status.scheduled != null) {

This keeps valid scheduled responses on the existing path and does not change public contracts, SDK types, server policy, persisted data, or daemon messages. The regression protects the user-visible Update control using the existing real component test harness; existing cases already cover non-null scheduled updates and cancellation.

The local fix changes two files, with 19 additions and 1 deletion: 20 changed text lines. It adds no dependency or generated file.

pnpm exec turbo run test --filter=@bb/app -- src/components/settings/UpdatesSettingsSection.test.tsx
Test Files  1 passed (1)
     Tests  76 passed (76)

pnpm exec turbo run typecheck lint --filter=@bb/app
exit 0; lint: 223 warnings, 0 errors

pnpm exec turbo run test --filter=@bb/app -- src/components/app-update/ src/hooks/useUpdateInventory.test.ts
Test Files  3 passed (3)
     Tests  21 passed (21)

pnpm exec turbo run build --filter=@bb/app
exit 0

pnpm exec oxfmt --check apps/app/src/components/settings/UpdatesSettingsSection.tsx apps/app/src/components/settings/UpdatesSettingsSection.test.tsx
All matched files use the correct format.

git diff --check
exit 0

git diff --numstat origin/main
18  0  apps/app/src/components/settings/UpdatesSettingsSection.test.tsx
1   1  apps/app/src/components/settings/UpdatesSettingsSection.tsx

7. Verification: second clean run

The same investigator created a second clean detached worktree at 88c78686805e828dda9f8d61fd5cd29aca26c138, verified its initial working tree was clean, installed frozen dependencies, and applied only the authored test patch. The exact focused command from section 4 exited 1 with the same requiredIdleMs exception at line 874. No production fix was present in this second run. No ports or data directory were needed because the reproduction is a jsdom component test, not a live service.

Both runs support the final rendering-defect verdict. No report correction was needed. This is a repeated clean reproduction by the same agent, not an independent review.

8. Related issues and pull requests

A search of open and closed issues for the failing field found only #7219. The issue timeline contained no linked pull request, and an open-PR search for the issue number returned none before implementing the local fix. No linked branch, patch, or test was executed.

9. Appendix and limits

Other checks used: git diff --check, git diff --numstat origin/main, relevant app-update and inventory tests, targeted app build, and formatting verification. Final results are recorded with the pull request.

> AGENT GENERATED