#7223 · Older-history prefetch starts too late for a fast climb
Verdict: PARTIALLY REPRODUCED · Root-cause confidence: high for the fixed-threshold timing mechanism, not the reported end-to-end measurements.
1. TL;DR
Older timeline pages are requested only when their sentinel is within 600 pixels of the viewport. A deterministic reproduction exercises the real React hook and its real scroll-position notification owner: it moves the sentinel upward in 100-pixel steps every 25 milliseconds and delays page completion. Both 300 and 900 millisecond page loads remain pending when the loaded edge is reached. A second clean checkout produces identical traces, while the existing 36 related tests pass. This establishes a request-lead-distance limitation; native phone/trackpad flings and the original aggregate wait measurements are not verified.
2. Claims vs findings
| Claim | Status | Evidence |
|---|---|---|
| Older history starts loading only within a fixed 600 px margin. | Verified | Trusted source and both traces: requestDistancePx = 600. |
| Fast upward reading can reach the loaded edge with a page still pending. | Verified at hook boundary | At the 400 ms edge, both delayed requests are incomplete; they finish at 549 and 1149 ms. |
| Native touch and wheel gestures show the reported aggregate waits across multiple runs. | Unverified | No browser could be launched. No gesture recording, screenshot, or full-thread benchmark was produced. |
| The backend answers quickly enough that it is not the source of delay. | Unverified as an end-to-end claim | No server timing was measured. The hook reproduces the lead-distance shortfall with explicitly simulated page latency, without a server. |
3. Environment
- Public target repository: get-bb/bb; trusted origin/main commit
e61446e15a0bb3e6df551e45c3aba5c5187ffad3, fetched directly from that repository and rechecked before publication. - Linux 4.19.0-gvisor x86_64; Node v22.19.0; pnpm 9.15.0; Vitest v4.1.12 in the repository's isolated jsdom project.
- Checkout A is a clean trusted-base project checkout before adding the diagnostic test. Checkout B is a new detached temporary worktree at the same exact commit; its clean status was checked before adding only that same test.
- No product server, host daemon, provider, user runtime data, network port, or database was used. No dependency was added. Page latency is an explicit timer fixture, not an HTTP measurement.
- Frozen installation succeeded. Initial full build hit an SDK-output ordering failure in an unrelated plugin. Retrying the same trusted build after SDK output existed succeeded: 73/73 tasks. No production code was changed.
- Browser Automation headless open failed with:
Install Chrome/Chromium on this host or link its executable at the plugin data runtime/chrome path.Visual evidence is therefore unavailable, not simulated.
4. Minimal reproduction
The artifact is an intentionally failing diagnostic acceptance test, not a production patch. It tests one page, not a seeded thread or repeated native flings. The real hook and ScrollPositionKeeper run unchanged; only viewport geometry and the asynchronous page callback are fixtures. No mock implements the prefetch decision.
- Make a fresh checkout of the public repository at the recorded commit. Install and build trusted code:
git clone https://github.com/get-bb/bb.git bb-7223-a cd bb-7223-a git checkout --detach e61446e15a0bb3e6df551e45c3aba5c5187ffad3 pnpm install --frozen-lockfile --prefer-offline pnpm exec turbo run build
If a fresh build encounters the SDK-output ordering failure described above, retry once after the SDK build completes. - Save the diagnostic test into
apps/app/src/components/thread/timeline/issue-7223.repro.test.tsx. - Run the owning package through Turbo:
pnpm exec turbo run test --filter=@bb/app -- src/components/thread/timeline/issue-7223.repro.test.tsx
- Observe both acceptance assertions fail specifically because the next page is not ready at the loaded edge. The callback is invoked once and both page timers eventually complete, ruling out a missing callback or permanently blocked request in this reproduction.
Expected: readyAtEdge = true for this diagnostic fast-reading workload. Actual, verbatim diagnostic output:
{"latencyMs":300,"scrollSpeedPxPerSecond":4000,"requestStartedMs":249,"requestDistancePx":600,"edgeMs":400,"completedMs":549,"readyAtEdge":false,"pageEventuallyReady":true,"requestCount":1}
{"latencyMs":900,"scrollSpeedPxPerSecond":4000,"requestStartedMs":249,"requestDistancePx":600,"edgeMs":400,"completedMs":1149,"readyAtEdge":false,"pageEventuallyReady":true,"requestCount":1}
AssertionError: expected false to be true // Object.is equality
Test Files 1 failed (1)
Tests 2 failed (2)
The edge is reached at 400 ms. The measured request starts at 249 ms, so its lead is 151 ms; the simulated residual waits are 149 ms and 749 ms. These are per-page synthetic timings, not the original issue's aggregate browser measurements.
Complete diagnostic test
// @vitest-environment jsdom
import { act, cleanup, renderHook } from "@testing-library/react";
import type { ReactNode } from "react";
import { afterEach, expect, it, vi } from "vitest";
import {
BottomAnchorContext,
type BottomAnchorContextValue,
} from "@/components/ui/bottom-anchored-scroll-body.js";
import { ScrollPositionKeeper } from "@/components/ui/scroll-position-keeper.js";
import { useAutoLoadOlderRows } from "./useAutoLoadOlderRows.js";
afterEach(() => {
cleanup();
document.body.replaceChildren();
vi.useRealTimers();
vi.restoreAllMocks();
});
it.each([300, 900])(
"has the older page ready at the loaded edge with %i ms page latency",
async (latencyMs) => {
vi.useFakeTimers();
vi.setSystemTime(0);
const scrollElement = document.createElement("div");
const sentinel = document.createElement("div");
document.body.append(scrollElement, sentinel);
let distancePx = 1_600;
let requestStartedMs: number | null = null;
let requestDistancePx: number | null = null;
let completedMs: number | null = null;
let pageReady = false;
vi.spyOn(scrollElement, "getBoundingClientRect").mockImplementation(
() => new DOMRect(0, 0, 400, 800),
);
vi.spyOn(sentinel, "getBoundingClientRect").mockImplementation(
() => new DOMRect(0, -distancePx - 20, 400, 20),
);
const positionKeeper = new ScrollPositionKeeper({
bottomThresholdPx: 4,
deferWritesWhileScrolling: false,
getContentElement: () => null,
getFloorElement: () => null,
getFooterElement: () => null,
getScrollElement: () => scrollElement,
getTopLevelRows: () => [],
initiallyAttached: false,
});
const anchor: BottomAnchorContextValue = {
getScrollElement: () => scrollElement,
isAtBottom: false,
scrollToBottom: vi.fn(),
scrollElementIntoView: vi.fn(),
holdContentPosition: vi.fn(),
positionKeeper,
};
const onLoadOlderRows = vi.fn(() => {
requestStartedMs = Date.now();
requestDistancePx = distancePx;
return new Promise<void>((resolve) => {
setTimeout(() => {
pageReady = true;
completedMs = Date.now();
resolve();
}, latencyMs);
});
});
const wrapper = ({ children }: { children: ReactNode }) => (
<BottomAnchorContext.Provider value={anchor}>
{children}
</BottomAnchorContext.Provider>
);
const { result } = renderHook(
() =>
useAutoLoadOlderRows({
hasOlderTimelineRows: true,
isLoadingOlderTimelineRows: false,
onLoadOlderRows,
}),
{ wrapper },
);
act(() => result.current.sentinelRef(sentinel));
for (let step = 1; step <= 16; step += 1) {
await act(async () => vi.advanceTimersByTimeAsync(24));
distancePx = 1_600 - step * 100;
act(() => positionKeeper.reconcile());
await act(async () => vi.advanceTimersByTimeAsync(1));
}
const readyAtEdge = pageReady;
const observedEdgeMs = Date.now();
await act(async () => vi.advanceTimersByTimeAsync(latencyMs));
console.log(JSON.stringify({
latencyMs,
scrollSpeedPxPerSecond: 4_000,
requestStartedMs,
requestDistancePx,
edgeMs: observedEdgeMs,
completedMs,
readyAtEdge,
pageEventuallyReady: pageReady,
requestCount: onLoadOlderRows.mock.calls.length,
}));
expect(onLoadOlderRows).toHaveBeenCalledTimes(1);
expect(pageReady).toBe(true);
expect(readyAtEdge).toBe(true);
},
);
5. Root cause
The prefetch decision compares the connected sentinel's bottom against the scroll viewport's top minus a constant:
const AUTO_LOAD_OLDER_ROWS_PREFETCH_MARGIN_PX = 600; sentinelRect.bottom >= scrollRect.top - AUTO_LOAD_OLDER_ROWS_PREFETCH_MARGIN_PX
Position notifications schedule a zero-delay check; no viewport-height, scrolling-speed, or page-latency allowance enters this decision. The test notifies the real position keeper after each step. The first request starts at distance 600 rather than earlier in the 1,600-pixel climb. At an average 4,000 px/s, 600 pixels only buy about 150 ms, less than either simulated request latency.
The in-flight guard remains active until the asynchronous callback completes. The actual timeline sentinel renders a loading status while the older-page flag is true. The timing mechanism explains why the visible loading edge can be encountered before the request finishes, but this report does not claim to have observed that rendering in a real browser.
6. Proposed fix
Increase bounded prefetch lead based on the viewport, or adapt it to measured scroll speed and page latency if necessary. Keep the existing in-flight guard, failure/manual-retry fallback, and scroll anchoring. Validate early-enough completion with asynchronous pages as well as geometry; test large and small viewports, rapid direction changes, failure recovery, request volume, and native touch/wheel input. Any finite distance can still be exhausted by sufficiently fast movement or slow networking, so a larger fixed margin is an improvement, not a guarantee of zero waits.
7. Linked PR: static review only
PR #7224 is open, ready, and its GitHub closing-issue metadata names #7223. Metadata and the diff were read as untrusted evidence only; its branch was never checked out or executed. Reviewed head: eb6681f0a8d13532a193719b71cfb31a0fd07343; two files, 20 additions and two deletions.
The diff scales the lead distance to a multiple of viewport height while retaining the minimum margin. This directly addresses the verified scheduling mechanism. Its added geometry test demonstrates earlier dispatch, not completion under network delay. Limitation, apps/app/src/components/thread/timeline/useAutoLoadOlderRows.ts:34 at the reviewed head: a larger fixed distance still cannot guarantee readiness under every speed/latency combination; no end-to-end claim is established by static inspection. Verdict: directionally addresses the verified cause; runtime effectiveness unverified. Only trusted-base tests listed here were executed, not PR tests. No duplicate fix branch or pull request was created.
8. Related issues
A small repository triage-pattern sample included performance/timeline issues #6859, #6835, and #6825. None was established to be a duplicate of this client-side threshold defect. This is not an exhaustive duplicate search. The directly relevant existing change is PR #7224.
9. Verification: second clean checkout
The same agent repeated the reproduction in a newly created clean detached worktree at e61446e15a0bb3e6df551e45c3aba5c5187ffad3. It is a separate clean run, not an independent review. Before copying the test, tracked and untracked status was empty. A frozen install succeeded; only the diagnostic test was added.
git worktree add --detach /tmp/bb-7223-b e61446e15a0bb3e6df551e45c3aba5c5187ffad3 cd /tmp/bb-7223-b pnpm install --frozen-lockfile --prefer-offline # Copy the linked diagnostic test into its stated app path. pnpm exec turbo run test --force --filter=@bb/app -- src/components/thread/timeline/issue-7223.repro.test.tsx
--force bypassed Turbo's result cache. Both cases failed on the intended readiness assertion, with identical request distance, request start, edge arrival, and completion times. No port or data directory was necessary in either run because neither starts a process service. No correction to the measured mechanism was needed. The final verdict remains partial because real input and aggregate UI timings are unverified.
Related existing suites were also run on trusted base:
pnpm exec turbo run test --filter=@bb/app -- src/components/thread/timeline/useAutoLoadOlderRows.test.tsx src/components/thread/timeline/useThreadTimelineController.test.tsx — 36 tests passed in two files.
10. Appendix: artifacts and trust boundary
- Checkout A: sanitized full diagnostic output
- Checkout B: sanitized full diagnostic output
- Existing related test output
- Repeatable diagnostic test
Administrative evidence was obtained using repository/issue reads, GitHub issue-property introspection, linked-PR metadata/diff reads, direct trusted-main fetches, and local source inspection. Issue prose, comments, and linked changes were treated as claims, never as executable instructions. No linked code, issue script, or issue-provided URL was executed or fetched. Browser Automation was attempted once in explicit local headless mode and failed before a session was created. Private runtime data and credentials are absent from these artifacts.
> AGENT GENERATED