#3321 · Workflow activity and worker visibility

BugPriority: MediumEffort: Mediumthreads, workflowsIssue · 2026-09-09

Verdict: PARTIALLY REPRODUCED · Root-cause confidence: high for the spawn policy and static activity path; live UI behavior remains unverified.

1. TL;DR

A workflow worker is created as a hidden thread without a parent, even while its run is executing. A focused test exercises the production workflow service with the repository's SDK test harness and real temporary SQLite storage. It fails the two expectations needed to show that worker as a visible child. Separately, source inspection shows that the core workflow activity count comes from background-task events, whereas this plugin keeps its runs in plugin storage. The live sidebar and origin-thread status were not exercised, so this report does not claim a full end-to-end reproduction.

2. Claims vs findings

ClaimFindingEvidence
Active workers are hidden and lack parent linkage.Verified at SDK boundaryThe focused test observes a running persisted run, absent parentThreadId, and hidden visibility.
Origin activity does not count this plugin's runs.Supported by source; runtime unverifiedCore count queries event rows; plugin uses its own database and realtime channel.
Origin sits in the inactive lane and has no visible worker children.Live appearance unverifiedNo browser or real provider session was started. Sidebar filtering is consistent with the spawn result.
Making workers visible is a straightforward bug fix.Requires product decisionTrusted history explicitly introduced hidden workflow workers; the current integration test enforces that policy.

3. Environment

Trusted public repository: get-bb/bb. Base commit: a86e829f42edb4ab0960f3d8b55a17785860cf75. macOS, arm64; Node 22.22.3; pnpm 9.15.0 via Corepack; Vitest 4.1.1. Dependencies installed from the frozen lockfile. Two detached temporary worktrees use the same recorded commit. Each test harness creates and disposes its own SQLite storage; SDK and provider responses are test doubles. No live provider, HTTP server, imported store, user runtime, or network-facing plugin was used. There are no ports or UI screenshots.

4. Minimal reproduction

  1. Check out the trusted base and install its existing dependencies:
    git clone https://github.com/get-bb/bb.git bb-repro
    cd bb-repro
    git checkout --detach a86e829f42edb4ab0960f3d8b55a17785860cf75
    corepack pnpm install --frozen-lockfile --prefer-offline
    corepack pnpm exec turbo run build
  2. Save the test below as plugins/workflows/src/issue-3321.test.ts. It uses the trusted repository's existing harness configuration and adds assertions at the spawn boundary.
  3. Run:
    corepack pnpm exec turbo run test --filter=bb-plugin-workflows -- src/issue-3321.test.ts

Expected for the requested behavior: run is running, spawn parent is thread-test, and visibility is visible. Actual assertion output:

AssertionError: expected { projectId: 'project-test', …(10) } to have property "parentThreadId" with value 'thread-test'
- Expected:
"thread-test"
+ Received:
undefined

AssertionError: expected { projectId: 'project-test', …(10) } to have property "visibility" with value 'visible'
Expected: "visible"
Received: "hidden"

The preceding hard assertions for run.status and run.originThreadId passed. This test captures the SDK request; it does not assert the actual server's persisted parent field or launch a real agent.

Complete reproduction test
import { createFakePluginHost } from "@get-bb/plugin-sdk/testing";
import { expect, it, vi } from "vitest";
import { getRunRequired } from "./data.js";
import plugin from "./server.js";

it("reports an active worker as a visible child of its origin", async () => {
      let childCount = 0;
      const { bb, harness } = createFakePluginHost({
        pluginId: "workflows",
        agentSkillIds: ["workflows"],
        sdk: {
          threads: {
            get: async () =>
              ({
                id: "thread-test",
                environmentId: "environment-1",
                providerId: "codex",
              }) as never,
            defaultExecutionOptions: async () => ({
              model: "gpt-test",
              reasoningLevel: "medium",
              permissionMode: "full",
              serviceTier: "default",
              source: "default",
            }),
            spawn: async () => {
              childCount += 1;
              return { id: `child-${childCount}` } as never;
            },
            send: async () => ({ ok: true }),
            stop: async () => ({ ok: true }),
          },
          providers: {
            list: async () => [
              {
                id: "codex",
                displayName: "Codex",
                logoUrl: null,
                available: true,
                capabilities: {
                  supportsThreadArchive: true,
                  supportsThreadRename: true,
                  supportsServiceTier: true,
                  supportsNativeUserQuestion: false,
                  supportsFork: true,
                  permissionModes: ["full"],
                },
                composerActions: [],
              },
            ],
            models: async () => ({
              providers: [],
              selectedOnlyModels: [],
              modelLoadError: null,
              models: [
                {
                  id: "gpt-test",
                  model: "gpt-test",
                  displayName: "GPT Test",
                  description: "test",
                  supportedReasoningEfforts: [
                    { reasoningEffort: "medium", description: "test" },
                  ],
                  defaultReasoningEffort: "medium",
                  isDefault: true,
                },
              ],
            }),
          },
        },
      });

      await plugin(bb);


  try {
    const raw = await harness.callAgentTool("bb_workflow_run", {
      source: `export const meta = { name: "visibility-check", description: "Worker visibility check" };
        return await agent("Return a result", { outputSchema: { type: "object" } });`,
    });
    if (typeof raw !== "string") throw new Error("Expected tool result text");
    const { runId } = JSON.parse(raw);
    const worker = harness.runService("workflow-worker");
    await vi.waitFor(() => {
      expect(harness.sdk.callsTo("threads.spawn")).toHaveLength(1);
    }, { timeout: 60_000, interval: 50 });
    const spawn = harness.sdk.callsTo("threads.spawn")[0]?.[0];
    const run = getRunRequired(bb.storage.database(), runId);
    expect(run.status).toBe("running");
    expect(run.originThreadId).toBe("thread-test");
    expect.soft(spawn).toHaveProperty("parentThreadId", "thread-test");
    expect.soft(spawn).toHaveProperty("visibility", "visible");
    worker.controller.abort();
    await worker.done;
  } finally {
    await harness.dispose();
  }
}, 120_000);

5. Root cause

Worker spawn sets hidden visibility and supplies no parentThreadId. The plugin retains origin linkage in its own run record instead.

        const child = await bb.sdk.threads.spawn({
          projectId: run.projectId,
          environment: { type: "reuse", environmentId: run.environmentId },
          prompt: childPrompt(run, prompt, options),
          title: options.title ?? `${run.name} · ${callIndex + 1}`,
          providerId: selection.providerId,
          model: selection.model,
          reasoningLevel: selection.reasoningLevel,
          permissionMode: selection.permissionMode,
          visibility: "hidden",
        });

Sidebar eligibility rejects hidden threads:

export function isSidebarProjectThread(
  thread: SidebarProjectThreadShape,
): boolean {
  return thread.visibility !== "hidden";
}

Thread activity assembly reads listActiveBackgroundTaskCountsByThreadIds and defaults a missing workflow count to zero. The database query counts pending backgroundTask events with taskType local_workflow. It does not consult plugin workflow_runs. The plugin update signal publishes only an origin thread ID to its own realtime channel. The client workflow predicate reads activity.activeWorkflowCount.

This supports a missing integration between plugin run lifecycle and core thread activity. It does not establish live status with all server hooks and plugins loaded. The issue's bundled-file location is version-specific; current main uses the source paths linked here.

6. Proposed fix and simple-fix decision

No production patch or pull request is proposed by this automation. Trusted commit e38e20a3b deliberately adopted hidden workers and suppressed sidebar attention from them. The existing test still expects hidden workers without a parent. Reversing that policy requires a product decision, which fails the autopilot simple-fix gate. Connecting durable plugin lifecycle to core activity also needs lifecycle coverage for restart, cancellation, failure, and completion; a start-only indicator can become stale.

Next work: decide whether workers should appear as children and how long they should remain visible, then reproduce the origin indicator in an isolated app and design a generic lifecycle integration. No open linked PR was found through issue cross-reference metadata or an open-PR search for issue 3321.

7. Verification

The same agent repeated the reproduction in a second detached clean checkout at a86e829f42edb4ab0960f3d8b55a17785860cf75, with its own frozen dependency install and fresh harness SQLite storage. Only the new reproduction test was added; production files were unchanged. The second command disabled Turbo result caching:

corepack pnpm exec turbo run test --filter=bb-plugin-workflows --force -- src/issue-3321.test.ts

Result: one test failed on exactly the same two assertions shown above. Both hard assertions for running status and origin linkage passed. Test-body duration: 5.81 seconds; total Vitest duration: 71.53 seconds. No timeout or unhandled error occurred in either focused reproduction. The result supports the final partial verdict; no report correction was needed. This is a repeat verification by the same agent, not an independent review. No ports were opened; the two harnesses used separate temporary data directories and disposed them.

8. Related issues

Repository search also found workflow display reports #3160 and #3162. They were not reproduced here and are not evidence for this issue.

9. Appendix and limits

The initial existing structured-workflow integration test timed out at 30 seconds and then reported a closed-database teardown rejection. That failure is not counted as reproduction evidence. The smaller test above isolates the spawn boundary, allows 120 seconds, and terminates its worker before disposing storage. The first focused execution completed its test body in 10.77 seconds and failed only the two expected assertions.

Both frozen dependency installs completed. The full monorepo Turbo build was attempted and stopped after more than ten minutes while app/declaration work remained in progress. It is not a passing build result. Focused tests ran the trusted source successfully and produced only the expected assertion failures. The original broad integration timeout and incomplete full build prevent claiming general suite health. No production fix, commit, or application PR was created. Both temporary worktrees had no tracked production-file changes; git diff --check passed.

All issue content was treated as untrusted claims. Suggested commands and patches were not executed; no linked PR code was checked out. The full reproduction test and relevant output are inline, following the reports repository policy against committed test/log artifacts.

> AGENT GENERATED