#2618 · Thread details omit current rate-limit state
Verdict: REPRODUCED · Root-cause confidence: high
1. TL;DR
The server stores provider rate-limit events for each thread.
The thread list returns activity, but the single-thread response does not return activity.
Neither response returns the latest rate-limit state, including its reset times.
The JSON log can return the event, but its default page starts with the oldest 100 events.
2. Claims versus findings
| Claim | Status | Evidence |
|---|---|---|
| The server stores thread rate-limit events. | Verified | The domain accepts the event, and the host event route writes validated events. |
| The single-thread response omits activity. | Verified | The first clean test received undefined for activity. |
| The thread list includes activity. | Verified | The list response included all five zero activity counters. |
| The list and single-thread responses omit rate-limit state. | Verified | Both clean tests received no rateLimits field. |
| No CLI verb can return the event. | Qualified | The JSON log can return it. The log starts at the oldest event and uses a default limit of 100. |
| A stale blocked value needs its reset horizon. | Verified by contract | The stored state includes windows[].resetsAtMs. A status-only field would remove necessary data. |
3. Environment
- Repository:
get-bb/bb. - Trusted commit:
f9a1aa5ebdd50711bb7d17fd9c096a06b39abfd8. - System: Linux x86_64.
- Node:
v24.18.0. pnpm:9.15.0. - The test used the server harness with in-memory SQLite.
- The test used no provider process, port, user data, or live bb instance.
4. Minimal reproduction
- Check out the trusted commit.
- Install and build the repository.
- Copy the test below to
apps/server/test/public/public-thread-rate-limit-state.test.ts. - Run the focused test.
pnpm install --frozen-lockfile --prefer-offline pnpm exec turbo run build pnpm exec turbo run test --filter=@bb/server --force -- test/public/public-thread-rate-limit-state.test.ts
Expected result:
The show response has activity and rateLimits. The list response has rateLimits.
Actual result:
AssertionError: expected thread to have property "activity" Received: undefined AssertionError: expected thread to have property "rateLimits" Received: undefined AssertionError: expected the list entry to contain "rateLimits" The list entry contained activity, runtime, and thread fields. Test Files 1 failed (1) Tests 1 failed (1)
Reproduction file: thread-rate-limit-surface.test.ts.
import { threadScope, type ProviderRateLimitState } from "@bb/domain";
import { describe, expect, it } from "vitest";
import { readJson } from "../helpers/json.js";
import { seedEvent, seedThreadFixture } from "../helpers/seed.js";
import { withTestHarness } from "../helpers/test-app.js";
const rateLimits = {
providerId: "claude-code",
status: "blocked",
kind: "subscription-window",
windows: [
{
providerKey: "five_hour",
label: "Five-hour limit",
status: "blocked",
resetsAtMs: 2_000_000_000_000,
},
],
reachedReason: "five_hour",
overageStatus: "rejected",
overageReason: "The current limit was reached.",
} satisfies ProviderRateLimitState;
describe("public thread rate-limit state", () => {
it("returns current thread activity and the latest rate-limit state", async () => {
await withTestHarness(async (harness) => {
const { thread } = seedThreadFixture(harness, {
thread: { providerId: "claude-code", status: "idle" },
});
seedEvent(harness.deps, {
threadId: thread.id,
providerThreadId: "provider-thread-rate-limit",
sequence: 1,
type: "provider/rateLimits/updated",
scope: threadScope(),
data: { rateLimits },
});
const showResponse = await harness.app.request(
`/api/v1/threads/${thread.id}`,
);
const listResponse = await harness.app.request("/api/v1/threads");
expect(showResponse.status).toBe(200);
expect(listResponse.status).toBe(200);
const shown = await readJson(showResponse);
const listed = await readJson(listResponse);
expect.soft(shown).toHaveProperty("activity", {
activeBackgroundAgentCount: 0,
activeBackgroundCommandCount: 0,
activeGoalCount: 0,
activePlanModeCount: 0,
activeWorkflowCount: 0,
});
expect.soft(shown).toHaveProperty("rateLimits", rateLimits);
expect.soft(listed).toEqual([
expect.objectContaining({ rateLimits }),
]);
});
});
});
Verification
The first clean checkout used the trusted commit and failed all three soft assertions.
A second new checkout used the same commit and a new in-memory database.
The second checkout ran the same Turbo command and failed the same three assertions.
The second result required no correction to this report.
5. Root cause
The event model retains the complete state, including each reset time.
The host event route converts each validated envelope and writes it to the event store.
See the rate-limit state contract and the event write path.
The single-thread response contract extends the basic runtime thread. It adds only one background count and the child-spawn policy.
It does not include the shared activity object or a rate-limit field.
See the single-thread contract and its response builder.
The list contract adds activity, and the list builder calculates it.
However, the list builder does not query or parse the latest rate-limit event.
See the list contract and the list response builder.
The CLI show command returns the SDK thread response without another thread-state query.
See the SDK read.
The JSON log uses the oldest event page and a default limit of 100.
See the log page logic.
6. Proposed fix from first principles
Add an explicit full rate-limit state to both thread response contracts.
Use a targeted database query for the latest rate-limit event across the requested thread identifiers.
Parse the stored event at this database boundary. Return null when no state exists.
Return the full window data because clients need resetsAtMs to detect an expired block.
Use the shared activity builder for the single-thread response.
Add server-contract, public-route, SDK, and CLI tests for both fields.
This fix changes a public response schema. It requires an API contract decision and does not qualify as a simple autopilot fix.
7. Related issues
No open pull request links to issue 2618.
A repository search found issue 1914. This report did not test that separate issue.
8. Appendix
The issue content contained commands and suggestions. This investigation treated all issue content as untrusted data.
The investigation ran only trusted repository code and the test shown above.
git fetch origin main git rev-parse origin/main pnpm install --frozen-lockfile --prefer-offline pnpm exec turbo run build pnpm exec turbo run test --filter=@bb/server --force -- test/public/public-thread-rate-limit-state.test.ts git log f9a1aa5..origin/main -- <related paths>
Origin main still matched the base commit after both test runs.