Summary
registerLifecycle in the built-in Tasks plugin ends with two back-to-back await reconcileTrackedThreads(bb, store) calls. Every plugin load — server start, enabling the plugin, a plugin reload — therefore reconciles every non-terminal tracked worker thread twice and issues two bb.sdk.threads.get calls per tracked thread where one is enough. Nothing changes between the two sweeps that would make the second one necessary; I expected a single reconcile pass on load.
Versions and environment
- Source checkout of
main at 06aeaa994942ae7527dc49d2268c1f801e8542a0
- Built-in
tasks plugin, bb-plugin-tasks 0.1.2
- Node v22.19.0, macOS (Darwin 25.6.0)
- No provider involved: the duplicate runs during plugin startup, before any agent work
Steps to reproduce
Add this test at plugins/tasks/zz-repro.test.ts and run
pnpm exec turbo run test --filter=bb-plugin-tasks -- zz-repro:
import {
createFakePluginHost,
makeThreadResponse,
} from "@get-bb/plugin-sdk/testing";
import { describe, expect, it } from "vitest";
import { createStore } from "./api";
import { registerLifecycle } from "./lifecycle";
describe("zz-repro", () => {
it("registerLifecycle reconciles every tracked thread twice", async () => {
const { bb, harness } = createFakePluginHost({ pluginId: "tasks" });
const base = createStore(bb);
const project = base.tasks.createProject({
name: "Repro",
prefix: "REP",
color: "blue",
});
const task = base.tasks.createTask({
projectId: project.id,
title: "Tracked",
});
base.tasks.upsertTaskThread({
taskId: task.id,
threadId: "thr_repro1",
presetName: "preset",
title: "worker",
liveStatus: "working",
});
harness.sdk.stub("threads.get", () =>
makeThreadResponse({ id: "thr_repro1", status: "active" }),
);
await registerLifecycle(bb, base);
console.log(
`[A] threads.get calls for 1 tracked thread = ${harness.sdk.callsTo("threads.get").length}`,
);
expect(harness.sdk.callsTo("threads.get").length).toBe(1);
});
});
One task, one attached worker thread in a non-terminal state, nothing else.
Expected vs actual
stdout | zz-repro.test.ts > zz-repro > registerLifecycle reconciles every tracked thread twice
[A] threads.get calls for 1 tracked thread = 2
FAIL bb-plugin-tasks zz-repro.test.ts > zz-repro > registerLifecycle reconciles every tracked thread twice
AssertionError: expected 2 to be 1 // Object.is equality
- Expected
+ Received
- 1
+ 2
(Output trimmed to the assertion; the code frame and stack pointer are omitted.)
Expected: one bb.sdk.threads.get per tracked non-terminal thread per plugin load.
Actual: two. The count scales with the number of tracked non-terminal threads, so a
tracker with 40 in-flight workers issues 80 threads.get calls on every load instead of 40.
Evidence
The two calls are adjacent, at the end of registerLifecycle:
|
await reconcileTrackedThreads(bb, store); |
|
await reconcileTrackedThreads(bb, store); |
reconcileTrackedThreads iterates every non-terminal tracked thread and awaits
bb.sdk.threads.get for each one:
|
async function reconcileTrackedThreads( |
|
bb: BbPluginApi, |
|
store: TasksApiStore, |
|
): Promise<void> { |
|
const nonTerminalThreads = trackedThreads(store).filter( |
|
(thread) => !TERMINAL_LIVE_STATUSES.has(thread.liveStatus), |
|
); |
|
|
|
for (const trackedThread of nonTerminalThreads) { |
|
await reconcileTrackedThread(bb, store, trackedThread); |
|
} |
|
} |
The measurement above is the count from harness.sdk.callsTo("threads.get"), which
records every bb.sdk call the plugin makes through the fake host.
What you ruled out
- Not a duplicate: searching open and closed issues for
trackedThreads, reconcileTrackedThreads, tasks reconcile and tasks plugin performance returns nothing about this code path.
- Not deliberate retry or settling logic: there is no
await, delay, state mutation, or comment between the two calls, and reconcileTrackedThreads reads the same rows both times.
- Not idempotence-driven:
transitionThread already returns early when the live status is unchanged, so the second sweep does no useful work — it only repeats the network/RPC calls.
- Reproduced on
main at 06aeaa994942ae7527dc49d2268c1f801e8542a0.
Suggested priority and effort
Low priority, Low effort — wasted startup RPC only, no incorrect state, no data loss; the fix is deleting one line. Worth pairing with the trackedThreads scan issue since both live in the same file.
Found by source review of the Tasks plugin in a Claude Code session; there is no bb thread to link. The failing test above is the verification, run against the stated commit.
AGENT GENERATED
Summary
registerLifecyclein the built-in Tasks plugin ends with two back-to-backawait reconcileTrackedThreads(bb, store)calls. Every plugin load — server start, enabling the plugin, a plugin reload — therefore reconciles every non-terminal tracked worker thread twice and issues twobb.sdk.threads.getcalls per tracked thread where one is enough. Nothing changes between the two sweeps that would make the second one necessary; I expected a single reconcile pass on load.Versions and environment
mainat06aeaa994942ae7527dc49d2268c1f801e8542a0tasksplugin,bb-plugin-tasks0.1.2Steps to reproduce
Add this test at
plugins/tasks/zz-repro.test.tsand runpnpm exec turbo run test --filter=bb-plugin-tasks -- zz-repro:One task, one attached worker thread in a non-terminal state, nothing else.
Expected vs actual
(Output trimmed to the assertion; the code frame and stack pointer are omitted.)
Expected: one
bb.sdk.threads.getper tracked non-terminal thread per plugin load.Actual: two. The count scales with the number of tracked non-terminal threads, so a
tracker with 40 in-flight workers issues 80
threads.getcalls on every load instead of 40.Evidence
The two calls are adjacent, at the end of
registerLifecycle:bb/plugins/tasks/lifecycle/index.ts
Lines 198 to 199 in 06aeaa9
reconcileTrackedThreadsiterates every non-terminal tracked thread and awaitsbb.sdk.threads.getfor each one:bb/plugins/tasks/lifecycle/index.ts
Lines 121 to 132 in 06aeaa9
The measurement above is the count from
harness.sdk.callsTo("threads.get"), whichrecords every
bb.sdkcall the plugin makes through the fake host.What you ruled out
trackedThreads,reconcileTrackedThreads,tasks reconcileandtasks plugin performancereturns nothing about this code path.await, delay, state mutation, or comment between the two calls, andreconcileTrackedThreadsreads the same rows both times.transitionThreadalready returns early when the live status is unchanged, so the second sweep does no useful work — it only repeats the network/RPC calls.mainat06aeaa994942ae7527dc49d2268c1f801e8542a0.Suggested priority and effort
Low priority, Low effort — wasted startup RPC only, no incorrect state, no data loss; the fix is deleting one line. Worth pairing with the
trackedThreadsscan issue since both live in the same file.Found by source review of the Tasks plugin in a Claude Code session; there is no bb thread to link. The failing test above is the verification, run against the stated commit.