← reports

#6118 · Child execution defaults bypass parent history

Bug Medium priority Medium effort providers cli · GitHub issue · October 22, 2026

Verdict: REPRODUCED · Root-cause confidence: high · Reproduction label: confirmed-repro

Trusted base: cc292b975fc35e16d7480cd9b8d55eff0ed88d8f

1. TL;DR

Creating a child without execution flags starts it with remembered project settings rather than its parent's last execution. This happens even when parent and project use the same provider: model and reasoning still come from the project. A real server creation test reproduces both variants, and the same test fails again in a second clean checkout. Provider selection has no parent input, and execution planning consults parent history only for permission mode. No real provider invocation or user runtime data is needed to demonstrate the wrong outbound start command.

2. Claims vs findings

ClaimFindingEvidence
Parent linkage does not imply provider inheritance.VerifiedA Claude Code parent produces a Codex child when project defaults select Codex.
Model and reasoning also bypass parent history.VerifiedBoth same-provider and cross-provider cases send project-model / medium instead of parent-model / low.
The guide promises parent execution precedence.VerifiedTrusted guide lines 81–89; history shows the broad wording was added alongside permission inheritance.
Parent-self conversion causes the failure.Ruled out by source and direct service reproductionCLI forwards parentThreadId; the server receives a valid parent but does not use it for these defaults.
Every newly spawned CLI thread rewrites project defaults.Refuted on trusted mainRemembering defaults is restricted to app-origin creation, not CLI-origin creation. This does not explain away the missing parent inheritance; the regression isolates an existing conflicting project preference directly.
A particular installed provider or model caused the reported live incident.UnverifiedNo account-backed turns or original private runtime records were accessed. The server's wrong command is reproduced with deterministic fixture models.

3. Environment

4. Minimal reproduction

  1. Check out the trusted base in a scratch directory:
    git clone https://github.com/get-bb/bb.git bb-6118-repro
    cd bb-6118-repro
    git checkout --detach cc292b975fc35e16d7480cd9b8d55eff0ed88d8f
    pnpm install --frozen-lockfile --prefer-offline
    pnpm exec turbo run build --filter=@get-bb/plugin-sdk
    pnpm exec turbo run build
  2. Save the complete reproduction test below at apps/server/test/threads/thread-create-parent-execution.test.ts. It seeds a parent execution, changes remembered project defaults, creates a child without provider/model/reasoning, and inspects the actual queued thread.start command.
  3. Run:
    pnpm exec turbo run test --filter=@bb/server -- test/threads/thread-create-parent-execution.test.ts

Expected: the two assertions pass with each parent's provider, parent-model, and low reasoning. Actual: both assertions fail on the unchanged production code. Cross-provider assertion output:

- Expected
+ Received
  {
-   "model": "parent-model",
-   "providerId": "claude-code",
-   "reasoningLevel": "low",
+   "model": "project-model",
+   "providerId": "codex",
+   "reasoningLevel": "medium",
  }
Test Files  1 failed (1)
     Tests  2 failed (2)

The same-provider case differs only in model and reasoning. Permission mode is held at auto in both fixtures so the failure cannot be attributed to a permission conversion.

Complete reproduction test
import { upsertProjectExecutionDefaults } from "@bb/db";
import { describe, expect, it } from "vitest";
import { createThreadFromRequest } from "../../src/services/threads/thread-create.js";
import { waitForQueuedCommand } from "../helpers/commands.js";
import { textInput } from "../helpers/prompt-input.js";
import {
  seedEnvironment,
  seedHostSession,
  seedProjectWithSource,
  seedThread,
  seedThreadRuntimeState,
} from "../helpers/seed.js";
import { withTestHarness } from "../helpers/test-app.js";

describe("child creation execution defaults", () => {
  it.each(["codex", "claude-code"])(
    "inherits %s parent execution ahead of remembered project defaults",
    async (parentProviderId) => {
      await withTestHarness(async (harness) => {
        const { host } = seedHostSession(harness.deps);
        const { project } = seedProjectWithSource(harness.deps, {
          hostId: host.id,
        });
        const environment = seedEnvironment(harness.deps, {
          hostId: host.id,
          projectId: project.id,
        });
        const parent = seedThread(harness.deps, {
          projectId: project.id,
          environmentId: environment.id,
          providerId: parentProviderId,
        });
        seedThreadRuntimeState(harness.deps, {
          threadId: parent.id,
          environmentId: environment.id,
          providerThreadId: "parent-session",
          model: "parent-model",
          reasoningLevel: "low",
          permissionMode: "auto",
        });
        upsertProjectExecutionDefaults(harness.db, {
          projectId: project.id,
          providerId: "codex",
          model: "project-model",
          reasoningLevel: "medium",
          permissionMode: "auto",
          serviceTier: "default",
        });

        const child = await createThreadFromRequest(harness.deps, {
          projectId: project.id,
          parentThreadId: parent.id,
          environment: { type: "reuse", environmentId: environment.id },
          input: textInput("Check execution defaults"),
          origin: "cli",
          startedOnBehalfOf: null,
        });
        const start = await waitForQueuedCommand(
          harness,
          ({ command }) =>
            command.type === "thread.start" && command.threadId === child.id,
        );
        expect(start.command.type).toBe("thread.start");
        if (start.command.type !== "thread.start") {
          throw new Error("Expected child start command");
        }
        expect({
          providerId: child.providerId,
          model: start.command.options.model,
          reasoningLevel: start.command.options.reasoningLevel,
        }).toEqual({
          providerId: parentProviderId,
          model: "parent-model",
          reasoningLevel: "low",
        });
      });
    },
  );
});

5. Root cause

The CLI forwards omitted execution fields as omissions and keeps the parent link (spawn.ts lines 602–619). The server validates and loads the parent, but calls its create-default resolver without that parent (parent validation; default resolver call).

Project create defaults consult only caller-owned input and project defaults. Provider policy then chooses:

args.requestedProviderId ?? storedProviderId ?? requireDefaultProviderId(registry)

The provider becomes immutable at creation. Later, execution planning loads parent history but model/reasoning candidates contain only request, child override, child last execution, and project execution. Only permission policy uses parentExecution. Thus an empty child history cannot supply the parent's selections, even when provider IDs match.

The guide contract is broader than the implementation. The introducing history added the parent-default wording while implementing only permission-history inheritance; this report does not infer that model/provider inheritance ever worked. The issue's separate claim about every CLI spawn updating remembered preferences is not true at this base: the remember policy accepts only app-origin creation outside reused environments and source-derived forks.

6. Proposed fix

Resolve creation defaults on the server using the already validated parent and its last recorded execution. Prefer caller-owned provider flags; otherwise select the parent's execution provider before project defaults. For a matching provider, use the parent's model and reasoning ahead of remembered project values, while leaving explicit model/reasoning overrides intact. Do not carry model or reasoning across an explicitly different provider. Keep the existing fallback when the parent has no recorded execution. Do not change permission policy, host protocol, stored schema, service-tier policy, or the public CLI/SDK contracts.

A focused regression should also cover explicit matching/different providers, explicit model/reasoning, source-aware stale picker values, and parents without history. Existing creation, runtime configuration, default policy, and execution-plan suites are the relevant sibling checks.

7. Verification

The same agent repeated the test in a second newly created detached worktree at the exact trusted base. Its production tree was clean before copying only the authored regression file. Frozen install, existing SDK build, and full build completed there. The same Turbo test command failed with the same two assertion mismatches (Vitest: 2 failed, 9.23 seconds). Each harness created fresh SQLite and data directories; no ports were used. This is a second clean reproduction by the same agent, not independent verification. No report correction was needed.

The complete original two-case test and the exact assertion mismatch are included in this HTML. Raw first-run and second-run logs are retained only in the local evidence backup; the public report repository deliberately excludes raw logs and test-file attachments.

8. Related issues

GitHub metadata reviewed: #3463 concerns one project-default row across providers; #4014 concerns the UI's project selection; #2306 concerns product-provider fallback. The same-provider reproduction distinguishes this defect from cross-provider project-default storage. No open PR linking #6118 was present when checked.

9. Appendix and limits

Commands used for evidence: trusted main fetch and git rev-parse; repository-file searches; git log -S and git show for history; frozen pnpm installs; Turbo SDK/full builds; the focused Turbo reproduction test in both checkouts; GitHub read-only issue/PR metadata. The report's linked logs retain failure output with machine-specific checkout paths removed. No screenshots are necessary for this nonvisual server-defaults bug.

The issue and its commands/links were treated as untrusted claims, not executed or fetched. The reproduction is authored from trusted repository tests and production code. It does not certify a real CLI/provider session, the original user's named threads, or the exact remembered-default write sequence. It establishes the server selection error before any provider can execute.