Skip to content

Timeline response cache admits nothing above 200 rows, so identical requests each rebuild the projection #3951

Description

@luketraas

Summary

The server's thread-timeline response cache skips any response with more than 200 top-level rows. Once a requested page of a long thread passes that size, two requests with an identical cache key (same revision and same request parameters — for example a second desktop window, or a plugin calling threads.timeline with the app's parameters) each run their own full projection instead of sharing one. I expected identical requests to share one build regardless of response size.

Versions and environment

  • bb 0.43.3 desktop app, macOS 26.5.2, Apple M5. Source read and repro run at c1a64f4.
  • Node v22.22.3 for the standalone repro.
  • Providers in the affected threads: Claude Code 2.1.278, codex-cli 0.155.1.
  • Setup: 2 desktop windows on one server, 561 threads, 2–10 running threads, 17 path-installed plugins.

Steps to reproduce

  1. Check out get-bb/bb at c1a64f4.
  2. Save this as repro-cache.ts in the checkout root:
    import { createThreadTimelineCache } from "./apps/server/src/services/threads/timeline-cache.ts";
    for (const n of [200, 201]) {
      const cache = createThreadTimelineCache();
      let builds = 0;
      const build = () => { builds += 1; return { rows: Array.from({ length: n }, (_, i) => ({ id: String(i) })) } as never; };
      cache.getOrBuild("thr_a", "same-key", build);
      cache.getOrBuild("thr_a", "same-key", build); // second client, identical key
      console.log(`rows=${n} builds=${builds} cacheSize=${cache.size}`);
    }
  3. Run node --experimental-strip-types repro-cache.ts from the checkout root.

Expected vs actual

Actual:   rows=200 builds=1 cacheSize=1
          rows=201 builds=2 cacheSize=0
Expected: rows=201 builds=1 cacheSize=1

Evidence

  • Admission rule:
    if (value.rows.length <= maxCacheableRows) {
    with the constant at #L6 (DEFAULT_MAX_CACHEABLE_ROWS = 200).
  • The key is maxSeq plus thread, status, environment, provider display name, page (kind, segment limit, cursor), nested rows, summary-only, diagnostic operations and completed-turn display:
    export function buildThreadTimelineParamsKey(
    args: Omit<ThreadTimelineCacheKeyArgs, "maxSeq">,
    ): string {
    return [
    args.threadId,
    args.status,
    args.environmentId ?? "-",
    args.providerDisplayName ?? "-",
    pageKeyPart(args.page),
    args.includeNestedRows ? "1" : "0",
    args.summaryOnly ? "1" : "0",
    args.includeDiagnosticOperations ? "1" : "0",
    args.completedTurnDisplay,
    ].join("|");
    }
    export function buildThreadTimelineCacheKey(
    args: ThreadTimelineCacheKeyArgs,
    ): string {
    return `${args.maxSeq}|${buildThreadTimelineParamsKey(args)}`;
    }
  • Context, not proof of the hit rate: on the live server, responses above the cutoff do occur for pageKind: "latest". From ~/.bb/logs/server.8.log, trimmed:
    {"threadId":"thr_5tpdgiiyp4","totalDurationMs":189.2,"thresholdMs":150,"selectionStrategy":"full","pageKind":"latest","eventRowCount":1320,"eventDataBytes":688143,"projectedRowCount":261,"responseRowCount":261,"msg":"Thread timeline build blocked the event loop"}
    {"threadId":"thr_tddwe6esxx","totalDurationMs":224.4,"thresholdMs":150,"selectionStrategy":"standard-window","pageKind":"latest","eventRowCount":1130,"eventDataBytes":703094,"projectedRowCount":298,"responseRowCount":298,"msg":"Thread timeline build blocked the event loop"}
    
    With 2 running threads the bb server used 24% of a core, with 10 running threads 84% (CPU-time deltas from ps -o cputime over fixed 40 s windows). I did not measure how many of those builds were identical-key misses, so I am not claiming this cutoff explains that load — only that above 200 rows the cache cannot help at all.
  • Investigation: bb thread thr_tddwe6esxx on my machine (local, not shareable by link); I can paste the measurement notes and scripts here on request.

What you ruled out

Suggested priority and effort

Affects long threads viewed by more than one client with identical request parameters. No workaround, no data loss. Replacing the row cutoff needs a memory-policy decision (rows can carry large nested content), so I am not suggesting an effort level.

AGENT GENERATED

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    confirmed-reproBug reproduced again from a clean trusted checkout; see linked reportperfthreadsTurns, timeline, messaging, forks

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions