← reports

#2836 · Thread continuation does not create child threads

Bug Medium Effort: High threads cli open on GitHub 2026-09-01 · base 1dfed079b

Verdict: REPRODUCED · Root-cause confidence: high

1. TL;DR

A thread continuation can produce a root thread instead of a child thread. The CLI omits the parent unless the caller supplies a parent option. The app handoff keeps the source only as prompt data. The sidebar list also has no model field. These three choices prevent the interface from showing a clear parent and model relation.

2. Claims vs findings

ClaimStatusEvidence
A CLI spawn in a thread omits the current thread as the parent.VerifiedThe focused regression test failed twice. The captured request had no parentThreadId.
An app handoff creates only a prompt link to the source thread.VerifiedThe handoff seed creates a thread mention. The root composer then builds a normal create request without a parent.
The thread list cannot show the selected model.VerifiedThe public thread list schema contains providerId, but it contains no model value. The row renders only the thread title.
The app replaces the focused pane during handoff.Verified by sourceThe handler uses project compose navigation. It does not use the split-pane helper.

3. Environment

4. Minimal reproduction

  1. Start at the trusted base commit.
  2. Save the test as apps/cli/src/__tests__/command-output/issue-2836-repro.test.ts.
  3. Run pnpm exec turbo run test --filter=@bb/cli -- --run src/__tests__/command-output/issue-2836-repro.test.ts.

Expected result:

The spawn request contains:
parentThreadId: "thread-context-parent"

Actual result:

Test Files  1 failed (1)
Tests       1 failed (1)
Received request fields did not include parentThreadId.
Number of calls: 1

Open the focused regression test.

import { describe, expect, it, vi } from "vitest";
import * as domain from "@bb/domain";
import {
  runCommand,
  setupCommandOutputTestEnvironment,
  stubServerApi,
} from "../helpers/command-output-harness.js";
import type { CommandRegistrar } from "../helpers/command-output-harness.js";
import * as fixtures from "../helpers/command-output-fixtures.js";
import { registerThreadCommands } from "../../commands/thread/index.js";

describe("thread spawn parent context", () => {
  setupCommandOutputTestEnvironment();

  const register: CommandRegistrar = (program) =>
    registerThreadCommands(program, () => "http://server");

  it("uses the current thread as the default parent", async () => {
    vi.stubEnv("BB_THREAD_ID", "thread-context-parent");
    const thread: domain.Thread = fixtures.makeThread({
      id: "thread-child",
      projectId: "proj-1",
      providerId: "codex",
      parentThreadId: "thread-context-parent",
    });
    const post = vi.fn(async () => thread);
    stubServerApi({ "v1.threads.$post": post });

    await runCommand(
      ["thread", "spawn", "--project", "proj-1", "--prompt", "review"],
      register,
    );

    expect(post).toHaveBeenCalledWith({
      json: expect.objectContaining({
        parentThreadId: "thread-context-parent",
      }),
    });
  });
});

5. Root cause

The CLI parent resolver reads BB_THREAD_ID only when parentSelf is true. Without that option, it returns only an explicit parent. The spawn request adds parentThreadId only when that resolver returns a value. See the parent resolver and the request builder.

The app handoff seed carries the source thread. However, it converts that value into a prompt mention. The root composer clears the fork state and later makes a normal request from the composer fields. It never adds the source as the parent. See the handoff seed, the seed consumer, and the create request.

The database can store a model override. The public thread list removes that value. Its schema and row have no model value, so the sidebar cannot render one. See the public thread mapping, the list schema, and the row title selection.

6. Proposed fix

First, select the parent default and the explicit opt-out behavior. Then make the CLI preserve an explicit parent and otherwise use the current thread. Keep the handoff source in composer state and add it to the create request as the parent. Use the existing split-pane path for the app handoff. Add the effective model to the thread list contract and show it on child rows. Update the CLI guide and focused tests with the selected policy.

7. Related issues

8. Verification

The same agent repeated the test in a second clean checkout at the exact base commit. The frozen install and full build passed. The focused test failed with the same missing field. No report correction was necessary.

First run result · Second run result

9. Appendix

Commands used:

pnpm install --frozen-lockfile --prefer-offline
pnpm exec turbo run build
pnpm exec turbo run test --filter=@bb/cli -- --run src/__tests__/command-output/issue-2836-repro.test.ts
git fetch origin main
git log 1dfed079b2b4bc06bdc45c96688e8e9b8a14a956..origin/main --oneline -- <related paths>

The issue data was treated as untrusted. The linked external prototype was not opened, fetched, or run.